Skip to content

Commit 144d191

Browse files
committed
src: add --optional-env-file flag
Fixes: nodejs#50993 Refs: nodejs#51451
1 parent dcebac8 commit 144d191

File tree

8 files changed

+90
-21
lines changed

8 files changed

+90
-21
lines changed

doc/api/cli.md

+11
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 does not exist, you
863+
can use the [`--optional-env-file`](#optional-env-fileconfig) flag instead.
864+
860865
### `-e`, `--eval "script"`
861866

862867
<!-- YAML
@@ -1692,6 +1697,12 @@ is being linked to Node.js. Sharing the OpenSSL configuration may have unwanted
16921697
implications and it is recommended to use a configuration section specific to
16931698
Node.js which is `nodejs_conf` and is default when this option is not used.
16941699

1700+
### `--optional-env-file=config`
1701+
1702+
Behaviour is the same as [`--env-file`](#env-fileconfig), but an error is not thrown if the file
1703+
does not exist.
1704+
1705+
16951706
### `--pending-deprecation`
16961707

16971708
<!-- YAML

src/node.cc

+7-6
Original file line numberDiff line numberDiff line change
@@ -832,20 +832,21 @@ static ExitCode InitializeNodeWithArgsInternal(
832832
HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
833833

834834
std::string node_options;
835-
auto file_paths = node::Dotenv::GetPathFromArgs(*argv);
835+
auto env_files = node::Dotenv::GetEnvFileDataFromArgs(*argv);
836836

837-
if (!file_paths.empty()) {
837+
if (!env_files.empty()) {
838838
CHECK(!per_process::v8_initialized);
839839

840-
for (const auto& file_path : file_paths) {
841-
switch (per_process::dotenv_file.ParsePath(file_path)) {
840+
for (const auto& file_data : env_files) {
841+
switch (per_process::dotenv_file.ParsePath(file_data.path)) {
842842
case Dotenv::ParseResult::Valid:
843843
break;
844844
case Dotenv::ParseResult::InvalidContent:
845-
errors->push_back(file_path + ": invalid format");
845+
errors->push_back(file_data.path + ": invalid format");
846846
break;
847847
case Dotenv::ParseResult::FileError:
848-
errors->push_back(file_path + ": not found");
848+
if (!file_data.is_required) continue;
849+
errors->push_back(file_data.path + ": not found");
849850
break;
850851
default:
851852
UNREACHABLE();

src/node_dotenv.cc

+31-14
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,51 @@ using v8::NewStringType;
1111
using v8::Object;
1212
using v8::String;
1313

14-
std::vector<std::string> Dotenv::GetPathFromArgs(
14+
std::vector<Dotenv::EnvFileData> Dotenv::GetEnvFileDataFromArgs(
1515
const std::vector<std::string>& args) {
16+
const std::string_view env_file_flag = "--env-file";
17+
1618
const auto find_match = [](const std::string& arg) {
17-
const std::string_view flag = "--env-file";
18-
return strncmp(arg.c_str(), flag.data(), flag.size()) == 0;
19+
const std::string_view env_file_flag = "--env-file";
20+
const std::string_view optional_env_file_flag = "--optional-env-file";
21+
return strncmp(arg.c_str(), env_file_flag.data(), env_file_flag.size()) == 0 ||
22+
strncmp(arg.c_str(),
23+
optional_env_file_flag.data(),
24+
optional_env_file_flag.size()) == 0;
1925
};
20-
std::vector<std::string> paths;
21-
auto path = std::find_if(args.begin(), args.end(), find_match);
2226

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

2634
if (equal_char != std::string::npos) {
27-
paths.push_back(path->substr(equal_char + 1));
35+
auto flag = matched_arg->substr(0, equal_char);
36+
struct EnvFileData env_file_data = {
37+
matched_arg->substr(equal_char + 1),
38+
strncmp(flag.c_str(), env_file_flag.data(), env_file_flag.size()) == 0
39+
};
40+
env_files.push_back(env_file_data);
2841
} else {
29-
auto next_path = std::next(path);
42+
auto file_path = std::next(matched_arg);
3043

31-
if (next_path == args.end()) {
32-
return paths;
44+
if (file_path == args.end()) {
45+
return env_files;
3346
}
3447

35-
paths.push_back(*next_path);
48+
struct EnvFileData env_file_data = {
49+
*file_path,
50+
strncmp(matched_arg->c_str(), env_file_flag.data(), env_file_flag.size()) == 0
51+
};
52+
env_files.push_back(env_file_data);
3653
}
3754

38-
path = std::find_if(++path, args.end(), find_match);
55+
matched_arg = std::find_if(++matched_arg, args.end(), find_match);
3956
}
4057

41-
return paths;
58+
return env_files;
4259
}
4360

4461
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 EnvFileData {
17+
std::string path;
18+
bool is_required;
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<EnvFileData> GetEnvFileDataFromArgs(
3135
const std::vector<std::string>& args);
3236

3337
private:

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
624624
"set environment variables from supplied file",
625625
&EnvironmentOptions::env_file);
626626
Implies("--env-file", "[has_env_file_string]");
627+
AddOption("--optional-env-file",
628+
"set environment variables from supplied file, but won't fail if the file doesn't exist",
629+
&EnvironmentOptions::optional_env_file);
630+
Implies("--optional-env-file", "[has_env_file_string]");
627631
AddOption("--test",
628632
"launch test runner on startup",
629633
&EnvironmentOptions::test_runner);

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ class EnvironmentOptions : public Options {
170170
std::string redirect_warnings;
171171
std::string diagnostic_dir;
172172
std::string env_file;
173+
std::string optional_env_file;
173174
bool has_env_file_string = false;
174175
bool test_runner = false;
175176
uint64_t test_runner_concurrency = 0;

test/fixtures/dotenv/optional.env

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
BASIC="OPTIONALLY LOADED"

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

+30
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const fixtures = require('../common/fixtures');
88

99
const validEnvFilePath = '../fixtures/dotenv/valid.env';
1010
const nodeOptionsEnvFilePath = '../fixtures/dotenv/node-options.env';
11+
const optionalEnvFilePath = '../fixtures/dotenv/optional.env';
1112

1213
describe('.env supports edge cases', () => {
1314

@@ -27,6 +28,22 @@ describe('.env supports edge cases', () => {
2728
assert.strictEqual(child.code, 0);
2829
});
2930

31+
it('supports multiple declarations, including optional ones', async () => {
32+
// process.env.BASIC is equal to `OPTIONALLY LOADED` because the third .env file overrides it.
33+
const code = `
34+
const assert = require('assert');
35+
assert.strictEqual(process.env.BASIC, 'OPTIONALLY LOADED');
36+
assert.strictEqual(process.env.NODE_NO_WARNINGS, '1');
37+
`.trim();
38+
const child = await common.spawnPromisified(
39+
process.execPath,
40+
[ `--env-file=${nodeOptionsEnvFilePath}`, `--optional-env-file=${optionalEnvFilePath}`, '--eval', code ],
41+
{ cwd: __dirname },
42+
);
43+
assert.strictEqual(child.stderr, '');
44+
assert.strictEqual(child.code, 0);
45+
});
46+
3047
it('supports absolute paths', async () => {
3148
const code = `
3249
require('assert').strictEqual(process.env.BASIC, 'basic');
@@ -52,6 +69,19 @@ describe('.env supports edge cases', () => {
5269
assert.strictEqual(child.code, 9);
5370
});
5471

72+
it('should handle non-existent optional .env file', async () => {
73+
const code = `
74+
require('assert').strictEqual(1, 1)
75+
`.trim();
76+
const child = await common.spawnPromisified(
77+
process.execPath,
78+
[ '--optional-env-file=.env', '--eval', code ],
79+
{ cwd: __dirname },
80+
);
81+
assert.strictEqual(child.stderr, '');
82+
assert.strictEqual(child.code, 0);
83+
});
84+
5585
it('should not override existing environment variables but introduce new vars', async () => {
5686
const code = `
5787
require('assert').strictEqual(process.env.BASIC, 'existing');

0 commit comments

Comments
 (0)