Skip to content

Commit 53ede87

Browse files
authored
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 9195210 commit 53ede87

File tree

7 files changed

+115
-32
lines changed

7 files changed

+115
-32
lines changed

doc/api/cli.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,8 @@ in the file, the value from the environment takes precedence.
828828
You can pass multiple `--env-file` arguments. Subsequent files override
829829
pre-existing variables defined in previous files.
830830

831+
An error is thrown if the file does not exist.
832+
831833
```bash
832834
node --env-file=.env --env-file=.development.env index.js
833835
```
@@ -867,6 +869,9 @@ Export keyword before a key is ignored:
867869
export USERNAME="nodejs" # will result in `nodejs` as the value.
868870
```
869871

872+
If you want to load environment variables from a file that may not exist, you
873+
can use the [`--env-file-if-exists`][] flag instead.
874+
870875
### `-e`, `--eval "script"`
871876

872877
<!-- YAML
@@ -1761,6 +1766,15 @@ is being linked to Node.js. Sharing the OpenSSL configuration may have unwanted
17611766
implications and it is recommended to use a configuration section specific to
17621767
Node.js which is `nodejs_conf` and is default when this option is not used.
17631768

1769+
### `--env-file-if-exists=config`
1770+
1771+
<!-- YAML
1772+
added: REPLACEME
1773+
-->
1774+
1775+
Behavior is the same as [`--env-file`][], but an error is not thrown if the file
1776+
does not exist.
1777+
17641778
### `--pending-deprecation`
17651779

17661780
<!-- YAML
@@ -3548,6 +3562,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
35483562
[`--build-snapshot`]: #--build-snapshot
35493563
[`--cpu-prof-dir`]: #--cpu-prof-dir
35503564
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
3565+
[`--env-file-if-exists`]: #--env-file-if-existsconfig
3566+
[`--env-file`]: #--env-fileconfig
35513567
[`--experimental-default-type=module`]: #--experimental-default-typetype
35523568
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
35533569
[`--experimental-strip-types`]: #--experimental-strip-types

src/node.cc

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -851,20 +851,26 @@ static ExitCode InitializeNodeWithArgsInternal(
851851
HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
852852

853853
std::string node_options;
854-
auto file_paths = node::Dotenv::GetPathFromArgs(*argv);
854+
auto env_files = node::Dotenv::GetDataFromArgs(*argv);
855855

856-
if (!file_paths.empty()) {
856+
if (!env_files.empty()) {
857857
CHECK(!per_process::v8_initialized);
858858

859-
for (const auto& file_path : file_paths) {
860-
switch (per_process::dotenv_file.ParsePath(file_path)) {
859+
for (const auto& file_data : env_files) {
860+
switch (per_process::dotenv_file.ParsePath(file_data.path)) {
861861
case Dotenv::ParseResult::Valid:
862862
break;
863863
case Dotenv::ParseResult::InvalidContent:
864-
errors->push_back(file_path + ": invalid format");
864+
errors->push_back(file_data.path + ": invalid format");
865865
break;
866866
case Dotenv::ParseResult::FileError:
867-
errors->push_back(file_path + ": not found");
867+
if (file_data.is_optional) {
868+
fprintf(stderr,
869+
"%s not found. Continuing without it.\n",
870+
file_data.path.c_str());
871+
continue;
872+
}
873+
errors->push_back(file_data.path + ": not found");
868874
break;
869875
default:
870876
UNREACHABLE();

src/node_dotenv.cc

Lines changed: 32 additions & 16 deletions
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

Lines changed: 5 additions & 1 deletion
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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
640640
"set environment variables from supplied file",
641641
&EnvironmentOptions::env_file);
642642
Implies("--env-file", "[has_env_file_string]");
643+
AddOption("--env-file-if-exists",
644+
"set environment variables from supplied file",
645+
&EnvironmentOptions::optional_env_file);
646+
Implies("--env-file-if-exists", "[has_env_file_string]");
643647
AddOption("--test",
644648
"launch test runner on startup",
645649
&EnvironmentOptions::test_runner);

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class EnvironmentOptions : public Options {
177177
std::string redirect_warnings;
178178
std::string diagnostic_dir;
179179
std::string env_file;
180+
std::string optional_env_file;
180181
bool has_env_file_string = false;
181182
bool test_runner = false;
182183
uint64_t test_runner_concurrency = 0;

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

Lines changed: 45 additions & 9 deletions
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)