Skip to content

Commit 84ffbaf

Browse files
committed
src: add --optional-env-file flag
Fixes: nodejs#50993 Refs: nodejs#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
1 parent 43f699d commit 84ffbaf

File tree

7 files changed

+108
-35
lines changed

7 files changed

+108
-35
lines changed

doc/api/cli.md

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

827+
An error is thrown if the file does not exist.
828+
827829
```bash
828830
node --env-file=.env --env-file=.development.env index.js
829831
```
@@ -863,6 +865,9 @@ Export keyword before a key is ignored:
863865
export USERNAME="nodejs" # will result in `nodejs` as the value.
864866
```
865867

868+
If you want to load environment variables from a file that may not exist, you
869+
can use the [`--env-file-if-exists`][] flag instead.
870+
866871
### `-e`, `--eval "script"`
867872

868873
<!-- YAML
@@ -1754,6 +1759,15 @@ is being linked to Node.js. Sharing the OpenSSL configuration may have unwanted
17541759
implications and it is recommended to use a configuration section specific to
17551760
Node.js which is `nodejs_conf` and is default when this option is not used.
17561761

1762+
### `--env-file-if-exists=config`
1763+
1764+
<!-- YAML
1765+
added: REPLACEME
1766+
-->
1767+
1768+
Behavior is the same as [`--env-file`][], but an error is not thrown if the file
1769+
does not exist.
1770+
17571771
### `--pending-deprecation`
17581772

17591773
<!-- YAML
@@ -3537,6 +3551,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
35373551
[`--build-snapshot`]: #--build-snapshot
35383552
[`--cpu-prof-dir`]: #--cpu-prof-dir
35393553
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
3554+
[`--env-file-if-exists`]: #--env-file-if-existsconfig
3555+
[`--env-file`]: #--env-fileconfig
35403556
[`--experimental-default-type=module`]: #--experimental-default-typetype
35413557
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
35423558
[`--experimental-strip-types`]: #--experimental-strip-types

src/node.cc

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

856856
std::string node_options;
857-
auto file_paths = node::Dotenv::GetPathFromArgs(*argv);
857+
auto env_files = node::Dotenv::GetDataFromArgs(*argv);
858858

859-
if (!file_paths.empty()) {
859+
if (!env_files.empty()) {
860860
CHECK(!per_process::v8_initialized);
861861

862-
for (const auto& file_path : file_paths) {
863-
switch (per_process::dotenv_file.ParsePath(file_path)) {
862+
for (const auto& file_data : env_files) {
863+
switch (per_process::dotenv_file.ParsePath(file_data.path)) {
864864
case Dotenv::ParseResult::Valid:
865865
break;
866866
case Dotenv::ParseResult::InvalidContent:
867-
errors->push_back(file_path + ": invalid format");
867+
errors->push_back(file_data.path + ": invalid format");
868868
break;
869869
case Dotenv::ParseResult::FileError:
870-
errors->push_back(file_path + ": not found");
870+
if (file_data.is_optional) {
871+
fprintf(stderr,
872+
"%s not found. Continuing without it.\n",
873+
file_data.path.c_str());
874+
continue;
875+
}
876+
errors->push_back(file_data.path + ": not found");
871877
break;
872878
default:
873879
UNREACHABLE();

src/node_dotenv.cc

+36-15
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,57 @@ 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 == "--" ||
20+
arg == "--env-file" ||
21+
arg.starts_with("--env-file=") ||
22+
arg == "--optional-env-file" ||
23+
arg.starts_with("--optional-env-file=");
1824
};
19-
std::vector<std::string> paths;
20-
auto path = std::find_if(args.begin(), args.end(), find_match);
2125

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

2837
if (equal_char != std::string::npos) {
29-
paths.push_back(path->substr(equal_char + 1));
38+
// `--env-file=path`
39+
auto flag = matched_arg->substr(0, equal_char);
40+
auto file_path = matched_arg->substr(equal_char + 1);
41+
struct env_file_data env_file_data = {
42+
file_path,
43+
flag.starts_with(optional_env_file_flag)
44+
};
45+
env_files.push_back(env_file_data);
3046
} else {
31-
auto next_path = std::next(path);
47+
// `--env-file path`
48+
auto file_path = std::next(matched_arg);
3249

33-
if (next_path == args.end()) {
34-
return paths;
50+
if (file_path == args.end()) {
51+
return env_files;
3552
}
3653

37-
paths.push_back(*next_path);
54+
struct env_file_data env_file_data = {
55+
*file_path,
56+
matched_arg->starts_with(optional_env_file_flag)
57+
};
58+
env_files.push_back(env_file_data);
3859
}
3960

40-
path = std::find_if(++path, args.end(), find_match);
61+
matched_arg = std::find_if(++matched_arg, args.end(), find_match);
4162
}
4263

43-
return paths;
64+
return env_files;
4465
}
4566

4667
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
@@ -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

+1
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

+34-13
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,29 @@ 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();
21-
const child = await common.spawnPromisified(
22-
process.execPath,
23-
[ `--env-file=${nodeOptionsEnvFilePath}`, `--env-file=${validEnvFilePath}`, '--eval', code ],
24-
{ cwd: __dirname },
25-
);
26-
assert.strictEqual(child.stderr, '');
27-
assert.strictEqual(child.code, 0);
19+
const result = 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'}=${path.resolve(__dirname, nodeOptionsEnvFilePath)}`,
27+
`${i & 0b10 ? '--env-file' : '--env-file-if-exists'}=${path.resolve(__dirname, validEnvFilePath)}`,
28+
'--eval', code,
29+
])));
30+
assert.deepStrictEqual(result, Array.from({ length: 4 }, () => ({
31+
code: 0,
32+
signal: null,
33+
stdout: '',
34+
stderr: '',
35+
})));
2836
});
2937

3038
it('supports absolute paths', async () => {
@@ -52,14 +60,27 @@ describe('.env supports edge cases', () => {
5260
assert.strictEqual(child.code, 9);
5361
});
5462

63+
it('should handle non-existent optional .env file', async () => {
64+
const code = `
65+
require('assert').strictEqual(1,1);
66+
`.trim();
67+
const child = await common.spawnPromisified(
68+
process.execPath,
69+
['--env-file-if-exists=.env', '--eval', code],
70+
{ cwd: __dirname },
71+
);
72+
assert.notStrictEqual(child.stderr, '');
73+
assert.strictEqual(child.code, 0);
74+
});
75+
5576
it('should not override existing environment variables but introduce new vars', async () => {
5677
const code = `
5778
require('assert').strictEqual(process.env.BASIC, 'existing');
5879
require('assert').strictEqual(process.env.AFTER_LINE, 'after_line');
5980
`.trim();
6081
const child = await common.spawnPromisified(
6182
process.execPath,
62-
[ `--env-file=${validEnvFilePath}`, '--eval', code ],
83+
[ `--env-file=${path.resolve(__dirname, validEnvFilePath)}`, '--eval', code ],
6384
{ cwd: __dirname, env: { ...process.env, BASIC: 'existing' } },
6485
);
6586
assert.strictEqual(child.stderr, '');
@@ -104,9 +125,9 @@ describe('.env supports edge cases', () => {
104125
process.execPath,
105126
[
106127
'--eval', `require('assert').strictEqual(process.env.BASIC, undefined);`,
107-
'--', '--env-file', validEnvFilePath,
128+
'--', '--env-file', path.resolve(__dirname, validEnvFilePath),
108129
],
109-
{ cwd: fixtures.path('dotenv') },
130+
{ cwd: __dirname },
110131
);
111132
assert.strictEqual(child.stdout, '');
112133
assert.strictEqual(child.stderr, '');

0 commit comments

Comments
 (0)