Skip to content

Commit 77d162a

Browse files
BoscoDomingoRafaelGSS
authored andcommitted
src: add --env-file-if-exists flag
Fixes: #50993 Refs: #51451 test: remove unnecessary comment src: conform to style guidelines src: change flag to `--env-file-optional` test: revert automatic linter changes doc: fix typos src: change flag to `--env-file-if-exists` src: refactor `env_file_data` and `GetEnvFileDataFromArgs` test: clean up tests src: print error when file not found test: remove unnecessary extras PR-URL: #53060 Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]>
1 parent b59c8b8 commit 77d162a

File tree

7 files changed

+115
-32
lines changed

7 files changed

+115
-32
lines changed

doc/api/cli.md

+16
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,8 @@ in the file, the value from the environment takes precedence.
818818
You can pass multiple `--env-file` arguments. Subsequent files override
819819
pre-existing variables defined in previous files.
820820

821+
An error is thrown if the file does not exist.
822+
821823
```bash
822824
node --env-file=.env --env-file=.development.env index.js
823825
```
@@ -857,6 +859,9 @@ Export keyword before a key is ignored:
857859
export USERNAME="nodejs" # will result in `nodejs` as the value.
858860
```
859861

862+
If you want to load environment variables from a file that may not exist, you
863+
can use the [`--env-file-if-exists`][] flag instead.
864+
860865
### `-e`, `--eval "script"`
861866

862867
<!-- YAML
@@ -1765,6 +1770,15 @@ is being linked to Node.js. Sharing the OpenSSL configuration may have unwanted
17651770
implications and it is recommended to use a configuration section specific to
17661771
Node.js which is `nodejs_conf` and is default when this option is not used.
17671772

1773+
### `--env-file-if-exists=config`
1774+
1775+
<!-- YAML
1776+
added: REPLACEME
1777+
-->
1778+
1779+
Behavior is the same as [`--env-file`][], but an error is not thrown if the file
1780+
does not exist.
1781+
17681782
### `--pending-deprecation`
17691783

17701784
<!-- YAML
@@ -3581,6 +3595,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
35813595
[`--build-snapshot`]: #--build-snapshot
35823596
[`--cpu-prof-dir`]: #--cpu-prof-dir
35833597
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
3598+
[`--env-file-if-exists`]: #--env-file-if-existsconfig
3599+
[`--env-file`]: #--env-fileconfig
35843600
[`--experimental-default-type=module`]: #--experimental-default-typetype
35853601
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
35863602
[`--experimental-strip-types`]: #--experimental-strip-types

src/node.cc

+12-6
Original file line numberDiff line numberDiff line change
@@ -903,20 +903,26 @@ static ExitCode InitializeNodeWithArgsInternal(
903903
HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
904904

905905
std::string node_options;
906-
auto file_paths = node::Dotenv::GetPathFromArgs(*argv);
906+
auto env_files = node::Dotenv::GetDataFromArgs(*argv);
907907

908-
if (!file_paths.empty()) {
908+
if (!env_files.empty()) {
909909
CHECK(!per_process::v8_initialized);
910910

911-
for (const auto& file_path : file_paths) {
912-
switch (per_process::dotenv_file.ParsePath(file_path)) {
911+
for (const auto& file_data : env_files) {
912+
switch (per_process::dotenv_file.ParsePath(file_data.path)) {
913913
case Dotenv::ParseResult::Valid:
914914
break;
915915
case Dotenv::ParseResult::InvalidContent:
916-
errors->push_back(file_path + ": invalid format");
916+
errors->push_back(file_data.path + ": invalid format");
917917
break;
918918
case Dotenv::ParseResult::FileError:
919-
errors->push_back(file_path + ": not found");
919+
if (file_data.is_optional) {
920+
fprintf(stderr,
921+
"%s not found. Continuing without it.\n",
922+
file_data.path.c_str());
923+
continue;
924+
}
925+
errors->push_back(file_data.path + ": not found");
920926
break;
921927
default:
922928
UNREACHABLE();

src/node_dotenv.cc

+32-16
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,52 @@ using v8::NewStringType;
1111
using v8::Object;
1212
using v8::String;
1313

14-
std::vector<std::string> Dotenv::GetPathFromArgs(
14+
std::vector<Dotenv::env_file_data> Dotenv::GetDataFromArgs(
1515
const std::vector<std::string>& args) {
16+
const std::string_view optional_env_file_flag = "--env-file-if-exists";
17+
1618
const auto find_match = [](const std::string& arg) {
17-
return arg == "--" || arg == "--env-file" || arg.starts_with("--env-file=");
19+
return arg == "--" || arg == "--env-file" ||
20+
arg.starts_with("--env-file=") || arg == "--env-file-if-exists" ||
21+
arg.starts_with("--env-file-if-exists=");
1822
};
19-
std::vector<std::string> paths;
20-
auto path = std::find_if(args.begin(), args.end(), find_match);
2123

22-
while (path != args.end()) {
23-
if (*path == "--") {
24-
return paths;
24+
std::vector<Dotenv::env_file_data> env_files;
25+
// This will be an iterator, pointing to args.end() if no matches are found
26+
auto matched_arg = std::find_if(args.begin(), args.end(), find_match);
27+
28+
while (matched_arg != args.end()) {
29+
if (*matched_arg == "--") {
30+
return env_files;
2531
}
26-
auto equal_char = path->find('=');
2732

28-
if (equal_char != std::string::npos) {
29-
paths.push_back(path->substr(equal_char + 1));
33+
auto equal_char_index = matched_arg->find('=');
34+
35+
if (equal_char_index != std::string::npos) {
36+
// `--env-file=path`
37+
auto flag = matched_arg->substr(0, equal_char_index);
38+
auto file_path = matched_arg->substr(equal_char_index + 1);
39+
40+
struct env_file_data env_file_data = {
41+
file_path, flag.starts_with(optional_env_file_flag)};
42+
env_files.push_back(env_file_data);
3043
} else {
31-
auto next_path = std::next(path);
44+
// `--env-file path`
45+
auto file_path = std::next(matched_arg);
3246

33-
if (next_path == args.end()) {
34-
return paths;
47+
if (file_path == args.end()) {
48+
return env_files;
3549
}
3650

37-
paths.push_back(*next_path);
51+
struct env_file_data env_file_data = {
52+
*file_path, matched_arg->starts_with(optional_env_file_flag)};
53+
env_files.push_back(env_file_data);
3854
}
3955

40-
path = std::find_if(++path, args.end(), find_match);
56+
matched_arg = std::find_if(++matched_arg, args.end(), find_match);
4157
}
4258

43-
return paths;
59+
return env_files;
4460
}
4561

4662
void Dotenv::SetEnvironment(node::Environment* env) {

src/node_dotenv.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ namespace node {
1313
class Dotenv {
1414
public:
1515
enum ParseResult { Valid, FileError, InvalidContent };
16+
struct env_file_data {
17+
std::string path;
18+
bool is_optional;
19+
};
1620

1721
Dotenv() = default;
1822
Dotenv(const Dotenv& d) = delete;
@@ -27,7 +31,7 @@ class Dotenv {
2731
void SetEnvironment(Environment* env);
2832
v8::Local<v8::Object> ToObject(Environment* env) const;
2933

30-
static std::vector<std::string> GetPathFromArgs(
34+
static std::vector<env_file_data> GetDataFromArgs(
3135
const std::vector<std::string>& args);
3236

3337
private:

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
652652
"set environment variables from supplied file",
653653
&EnvironmentOptions::env_file);
654654
Implies("--env-file", "[has_env_file_string]");
655+
AddOption("--env-file-if-exists",
656+
"set environment variables from supplied file",
657+
&EnvironmentOptions::optional_env_file);
658+
Implies("--env-file-if-exists", "[has_env_file_string]");
655659
AddOption("--test",
656660
"launch test runner on startup",
657661
&EnvironmentOptions::test_runner);

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ class EnvironmentOptions : public Options {
178178
std::string redirect_warnings;
179179
std::string diagnostic_dir;
180180
std::string env_file;
181+
std::string optional_env_file;
181182
bool has_env_file_string = false;
182183
bool test_runner = false;
183184
uint64_t test_runner_concurrency = 0;

test/parallel/test-dotenv-edge-cases.js

+45-9
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,53 @@ const validEnvFilePath = '../fixtures/dotenv/valid.env';
1010
const nodeOptionsEnvFilePath = '../fixtures/dotenv/node-options.env';
1111

1212
describe('.env supports edge cases', () => {
13-
14-
it('supports multiple declarations', async () => {
15-
// process.env.BASIC is equal to `basic` because the second .env file overrides it.
13+
it('supports multiple declarations, including optional ones', async () => {
1614
const code = `
1715
const assert = require('assert');
1816
assert.strictEqual(process.env.BASIC, 'basic');
1917
assert.strictEqual(process.env.NODE_NO_WARNINGS, '1');
2018
`.trim();
19+
const children = await Promise.all(Array.from({ length: 4 }, (_, i) =>
20+
common.spawnPromisified(
21+
process.execPath,
22+
[
23+
// Bitwise AND to create all 4 possible combinations:
24+
// i & 0b01 is truthy when i has value 0bx1 (i.e. 0b01 (1) and 0b11 (3)), falsy otherwise.
25+
// i & 0b10 is truthy when i has value 0b1x (i.e. 0b10 (2) and 0b11 (3)), falsy otherwise.
26+
`${i & 0b01 ? '--env-file' : '--env-file-if-exists'}=${nodeOptionsEnvFilePath}`,
27+
`${i & 0b10 ? '--env-file' : '--env-file-if-exists'}=${validEnvFilePath}`,
28+
'--eval', code,
29+
],
30+
{ cwd: __dirname },
31+
)));
32+
assert.deepStrictEqual(children, Array.from({ length: 4 }, () => ({
33+
code: 0,
34+
signal: null,
35+
stdout: '',
36+
stderr: '',
37+
})));
38+
});
39+
40+
it('supports absolute paths', async () => {
41+
const code = `
42+
require('assert').strictEqual(process.env.BASIC, 'basic');
43+
`.trim();
2144
const child = await common.spawnPromisified(
2245
process.execPath,
23-
[ `--env-file=${nodeOptionsEnvFilePath}`, `--env-file=${validEnvFilePath}`, '--eval', code ],
24-
{ cwd: __dirname },
46+
[ `--env-file=${path.resolve(__dirname, validEnvFilePath)}`, '--eval', code ],
2547
);
2648
assert.strictEqual(child.stderr, '');
2749
assert.strictEqual(child.code, 0);
2850
});
2951

30-
it('supports absolute paths', async () => {
52+
it('supports a space instead of \'=\' for the flag ', async () => {
3153
const code = `
3254
require('assert').strictEqual(process.env.BASIC, 'basic');
3355
`.trim();
3456
const child = await common.spawnPromisified(
3557
process.execPath,
36-
[ `--env-file=${path.resolve(__dirname, validEnvFilePath)}`, '--eval', code ],
58+
[ '--env-file', validEnvFilePath, '--eval', code ],
59+
{ cwd: __dirname },
3760
);
3861
assert.strictEqual(child.stderr, '');
3962
assert.strictEqual(child.code, 0);
@@ -48,10 +71,23 @@ describe('.env supports edge cases', () => {
4871
[ '--env-file=.env', '--eval', code ],
4972
{ cwd: __dirname },
5073
);
51-
assert.notStrictEqual(child.stderr.toString(), '');
74+
assert.notStrictEqual(child.stderr, '');
5275
assert.strictEqual(child.code, 9);
5376
});
5477

78+
it('should handle non-existent optional .env file', async () => {
79+
const code = `
80+
require('assert').strictEqual(1,1);
81+
`.trim();
82+
const child = await common.spawnPromisified(
83+
process.execPath,
84+
['--env-file-if-exists=.env', '--eval', code],
85+
{ cwd: __dirname },
86+
);
87+
assert.notStrictEqual(child.stderr, '');
88+
assert.strictEqual(child.code, 0);
89+
});
90+
5591
it('should not override existing environment variables but introduce new vars', async () => {
5692
const code = `
5793
require('assert').strictEqual(process.env.BASIC, 'existing');
@@ -106,7 +142,7 @@ describe('.env supports edge cases', () => {
106142
'--eval', `require('assert').strictEqual(process.env.BASIC, undefined);`,
107143
'--', '--env-file', validEnvFilePath,
108144
],
109-
{ cwd: fixtures.path('dotenv') },
145+
{ cwd: __dirname },
110146
);
111147
assert.strictEqual(child.stdout, '');
112148
assert.strictEqual(child.stderr, '');

0 commit comments

Comments
 (0)