Skip to content

Commit b8c89a6

Browse files
BoscoDomingomarco-ippolito
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 Backport-PR-URL: #56932 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 9097de0 commit b8c89a6

File tree

7 files changed

+122
-32
lines changed

7 files changed

+122
-32
lines changed

doc/api/cli.md

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

804+
An error is thrown if the file does not exist.
805+
804806
```bash
805807
node --env-file=.env --env-file=.development.env index.js
806808
```
@@ -840,6 +842,9 @@ Export keyword before a key is ignored:
840842
export USERNAME="nodejs" # will result in `nodejs` as the value.
841843
```
842844

845+
If you want to load environment variables from a file that may not exist, you
846+
can use the [`--env-file-if-exists`][] flag instead.
847+
843848
### `-e`, `--eval "script"`
844849

845850
<!-- YAML
@@ -1678,6 +1683,15 @@ is being linked to Node.js. Sharing the OpenSSL configuration may have unwanted
16781683
implications and it is recommended to use a configuration section specific to
16791684
Node.js which is `nodejs_conf` and is default when this option is not used.
16801685

1686+
### `--env-file-if-exists=config`
1687+
1688+
<!-- YAML
1689+
added: REPLACEME
1690+
-->
1691+
1692+
Behavior is the same as [`--env-file`][], but an error is not thrown if the file
1693+
does not exist.
1694+
16811695
### `--pending-deprecation`
16821696

16831697
<!-- YAML
@@ -3354,6 +3368,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
33543368
[`--build-snapshot`]: #--build-snapshot
33553369
[`--cpu-prof-dir`]: #--cpu-prof-dir
33563370
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
3371+
[`--env-file-if-exists`]: #--env-file-if-existsconfig
3372+
[`--env-file`]: #--env-fileconfig
33573373
[`--experimental-default-type=module`]: #--experimental-default-typetype
33583374
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
33593375
[`--experimental-wasm-modules`]: #--experimental-wasm-modules

src/node.cc

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

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

906-
if (!file_paths.empty()) {
906+
if (!env_files.empty()) {
907907
CHECK(!per_process::v8_initialized);
908908

909-
for (const auto& file_path : file_paths) {
910-
switch (per_process::dotenv_file.ParsePath(file_path)) {
909+
for (const auto& file_data : env_files) {
910+
switch (per_process::dotenv_file.ParsePath(file_data.path)) {
911911
case Dotenv::ParseResult::Valid:
912912
break;
913913
case Dotenv::ParseResult::InvalidContent:
914-
errors->push_back(file_path + ": invalid format");
914+
errors->push_back(file_data.path + ": invalid format");
915915
break;
916916
case Dotenv::ParseResult::FileError:
917-
errors->push_back(file_path + ": not found");
917+
if (file_data.is_optional) {
918+
fprintf(stderr,
919+
"%s not found. Continuing without it.\n",
920+
file_data.path.c_str());
921+
continue;
922+
}
923+
errors->push_back(file_data.path + ": not found");
918924
break;
919925
default:
920926
UNREACHABLE();

src/node_dotenv.cc

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,66 @@ 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) {
1719
auto arg_chars = arg.c_str();
1820
auto arg_len = arg.size();
1921
if (arg_chars[0] != '-' || arg_chars[1] != '-') return false;
2022
if (arg_len == 2) return true; // arg == "--"
2123
const std::string_view flag = "env-file";
22-
const auto len = flag.size();
24+
auto len = flag.size();
2325
if (strncmp(arg_chars + 2, flag.data(), len) != 0) return false;
26+
if (arg_len == 2 + len) return true;
27+
const std::string_view flag2 = "-if-exists";
28+
if (strncmp(arg_chars + 2 + len, flag2.data(), flag2.size()) == 0)
29+
len += flag2.size();
2430
return arg_len == 2 + len || arg_chars[2 + len] == '=';
2531
};
26-
std::vector<std::string> paths;
27-
auto path = std::find_if(args.begin(), args.end(), find_match);
2832

29-
while (path != args.end()) {
30-
if (path->size() == 2 && strncmp(path->c_str(), "--", 2) == 0) {
31-
return paths;
33+
std::vector<Dotenv::env_file_data> env_files;
34+
// This will be an iterator, pointing to args.end() if no matches are found
35+
auto matched_arg = std::find_if(args.begin(), args.end(), find_match);
36+
37+
while (matched_arg != args.end()) {
38+
if (matched_arg->size() == 2 &&
39+
strncmp(matched_arg->c_str(), "--", 2) == 0) {
40+
return env_files;
3241
}
33-
auto equal_char = path->find('=');
3442

35-
if (equal_char != std::string::npos) {
36-
paths.push_back(path->substr(equal_char + 1));
43+
auto equal_char_index = matched_arg->find('=');
44+
45+
if (equal_char_index != std::string::npos) {
46+
// `--env-file=path`
47+
auto flag = matched_arg->substr(0, equal_char_index);
48+
auto file_path = matched_arg->substr(equal_char_index + 1);
49+
50+
struct env_file_data env_file_data = {
51+
file_path, strncmp(matched_arg->c_str(),
52+
optional_env_file_flag.data(),
53+
optional_env_file_flag.size()) == 0};
54+
env_files.push_back(env_file_data);
3755
} else {
38-
auto next_path = std::next(path);
56+
// `--env-file path`
57+
auto file_path = std::next(matched_arg);
3958

40-
if (next_path == args.end()) {
41-
return paths;
59+
if (file_path == args.end()) {
60+
return env_files;
4261
}
4362

44-
paths.push_back(*next_path);
63+
struct env_file_data env_file_data = {
64+
*file_path, strncmp(matched_arg->c_str(),
65+
optional_env_file_flag.data(),
66+
optional_env_file_flag.size()) == 0};
67+
env_files.push_back(env_file_data);
4568
}
4669

47-
path = std::find_if(++path, args.end(), find_match);
70+
matched_arg = std::find_if(++matched_arg, args.end(), find_match);
4871
}
4972

50-
return paths;
73+
return env_files;
5174
}
5275

5376
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
@@ -663,6 +663,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
663663
"set environment variables from supplied file",
664664
&EnvironmentOptions::env_file);
665665
Implies("--env-file", "[has_env_file_string]");
666+
AddOption("--env-file-if-exists",
667+
"set environment variables from supplied file",
668+
&EnvironmentOptions::optional_env_file);
669+
Implies("--env-file-if-exists", "[has_env_file_string]");
666670
AddOption("--test",
667671
"launch test runner on startup",
668672
&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', '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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy