0
0
mirror of https://github.com/nodejs/node.git synced 2024-11-21 21:19:50 +01:00

src: add --env-file-if-exists flag

Fixes: https://github.com/nodejs/node/issues/50993
Refs: https://github.com/nodejs/node/issues/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: https://github.com/nodejs/node/pull/53060
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
Bosco Domingo 2024-09-16 02:58:16 +02:00 committed by GitHub
parent 9195210399
commit 53ede878a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 118 additions and 35 deletions

View File

@ -828,6 +828,8 @@ in the file, the value from the environment takes precedence.
You can pass multiple `--env-file` arguments. Subsequent files override
pre-existing variables defined in previous files.
An error is thrown if the file does not exist.
```bash
node --env-file=.env --env-file=.development.env index.js
```
@ -867,6 +869,9 @@ Export keyword before a key is ignored:
export USERNAME="nodejs" # will result in `nodejs` as the value.
```
If you want to load environment variables from a file that may not exist, you
can use the [`--env-file-if-exists`][] flag instead.
### `-e`, `--eval "script"`
<!-- YAML
@ -1761,6 +1766,15 @@ is being linked to Node.js. Sharing the OpenSSL configuration may have unwanted
implications and it is recommended to use a configuration section specific to
Node.js which is `nodejs_conf` and is default when this option is not used.
### `--env-file-if-exists=config`
<!-- YAML
added: REPLACEME
-->
Behavior is the same as [`--env-file`][], but an error is not thrown if the file
does not exist.
### `--pending-deprecation`
<!-- YAML
@ -3548,6 +3562,8 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--build-snapshot`]: #--build-snapshot
[`--cpu-prof-dir`]: #--cpu-prof-dir
[`--diagnostic-dir`]: #--diagnostic-dirdirectory
[`--env-file-if-exists`]: #--env-file-if-existsconfig
[`--env-file`]: #--env-fileconfig
[`--experimental-default-type=module`]: #--experimental-default-typetype
[`--experimental-sea-config`]: single-executable-applications.md#generating-single-executable-preparation-blobs
[`--experimental-strip-types`]: #--experimental-strip-types

View File

@ -851,20 +851,26 @@ static ExitCode InitializeNodeWithArgsInternal(
HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
std::string node_options;
auto file_paths = node::Dotenv::GetPathFromArgs(*argv);
auto env_files = node::Dotenv::GetDataFromArgs(*argv);
if (!file_paths.empty()) {
if (!env_files.empty()) {
CHECK(!per_process::v8_initialized);
for (const auto& file_path : file_paths) {
switch (per_process::dotenv_file.ParsePath(file_path)) {
for (const auto& file_data : env_files) {
switch (per_process::dotenv_file.ParsePath(file_data.path)) {
case Dotenv::ParseResult::Valid:
break;
case Dotenv::ParseResult::InvalidContent:
errors->push_back(file_path + ": invalid format");
errors->push_back(file_data.path + ": invalid format");
break;
case Dotenv::ParseResult::FileError:
errors->push_back(file_path + ": not found");
if (file_data.is_optional) {
fprintf(stderr,
"%s not found. Continuing without it.\n",
file_data.path.c_str());
continue;
}
errors->push_back(file_data.path + ": not found");
break;
default:
UNREACHABLE();

View File

@ -11,36 +11,52 @@ using v8::NewStringType;
using v8::Object;
using v8::String;
std::vector<std::string> Dotenv::GetPathFromArgs(
std::vector<Dotenv::env_file_data> Dotenv::GetDataFromArgs(
const std::vector<std::string>& args) {
const std::string_view optional_env_file_flag = "--env-file-if-exists";
const auto find_match = [](const std::string& arg) {
return arg == "--" || arg == "--env-file" || arg.starts_with("--env-file=");
return arg == "--" || arg == "--env-file" ||
arg.starts_with("--env-file=") || arg == "--env-file-if-exists" ||
arg.starts_with("--env-file-if-exists=");
};
std::vector<std::string> paths;
auto path = std::find_if(args.begin(), args.end(), find_match);
while (path != args.end()) {
if (*path == "--") {
return paths;
std::vector<Dotenv::env_file_data> env_files;
// This will be an iterator, pointing to args.end() if no matches are found
auto matched_arg = std::find_if(args.begin(), args.end(), find_match);
while (matched_arg != args.end()) {
if (*matched_arg == "--") {
return env_files;
}
auto equal_char = path->find('=');
if (equal_char != std::string::npos) {
paths.push_back(path->substr(equal_char + 1));
auto equal_char_index = matched_arg->find('=');
if (equal_char_index != std::string::npos) {
// `--env-file=path`
auto flag = matched_arg->substr(0, equal_char_index);
auto file_path = matched_arg->substr(equal_char_index + 1);
struct env_file_data env_file_data = {
file_path, flag.starts_with(optional_env_file_flag)};
env_files.push_back(env_file_data);
} else {
auto next_path = std::next(path);
// `--env-file path`
auto file_path = std::next(matched_arg);
if (next_path == args.end()) {
return paths;
if (file_path == args.end()) {
return env_files;
}
paths.push_back(*next_path);
struct env_file_data env_file_data = {
*file_path, matched_arg->starts_with(optional_env_file_flag)};
env_files.push_back(env_file_data);
}
path = std::find_if(++path, args.end(), find_match);
matched_arg = std::find_if(++matched_arg, args.end(), find_match);
}
return paths;
return env_files;
}
void Dotenv::SetEnvironment(node::Environment* env) {

View File

@ -13,6 +13,10 @@ namespace node {
class Dotenv {
public:
enum ParseResult { Valid, FileError, InvalidContent };
struct env_file_data {
std::string path;
bool is_optional;
};
Dotenv() = default;
Dotenv(const Dotenv& d) = delete;
@ -27,7 +31,7 @@ class Dotenv {
void SetEnvironment(Environment* env);
v8::Local<v8::Object> ToObject(Environment* env) const;
static std::vector<std::string> GetPathFromArgs(
static std::vector<env_file_data> GetDataFromArgs(
const std::vector<std::string>& args);
private:

View File

@ -640,6 +640,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"set environment variables from supplied file",
&EnvironmentOptions::env_file);
Implies("--env-file", "[has_env_file_string]");
AddOption("--env-file-if-exists",
"set environment variables from supplied file",
&EnvironmentOptions::optional_env_file);
Implies("--env-file-if-exists", "[has_env_file_string]");
AddOption("--test",
"launch test runner on startup",
&EnvironmentOptions::test_runner);

View File

@ -177,6 +177,7 @@ class EnvironmentOptions : public Options {
std::string redirect_warnings;
std::string diagnostic_dir;
std::string env_file;
std::string optional_env_file;
bool has_env_file_string = false;
bool test_runner = false;
uint64_t test_runner_concurrency = 0;

View File

@ -10,21 +10,31 @@ const validEnvFilePath = '../fixtures/dotenv/valid.env';
const nodeOptionsEnvFilePath = '../fixtures/dotenv/node-options.env';
describe('.env supports edge cases', () => {
it('supports multiple declarations', async () => {
// process.env.BASIC is equal to `basic` because the second .env file overrides it.
it('supports multiple declarations, including optional ones', async () => {
const code = `
const assert = require('assert');
assert.strictEqual(process.env.BASIC, 'basic');
assert.strictEqual(process.env.NODE_NO_WARNINGS, '1');
`.trim();
const child = await common.spawnPromisified(
process.execPath,
[ `--env-file=${nodeOptionsEnvFilePath}`, `--env-file=${validEnvFilePath}`, '--eval', code ],
{ cwd: __dirname },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
const children = await Promise.all(Array.from({ length: 4 }, (_, i) =>
common.spawnPromisified(
process.execPath,
[
// Bitwise AND to create all 4 possible combinations:
// i & 0b01 is truthy when i has value 0bx1 (i.e. 0b01 (1) and 0b11 (3)), falsy otherwise.
// i & 0b10 is truthy when i has value 0b1x (i.e. 0b10 (2) and 0b11 (3)), falsy otherwise.
`${i & 0b01 ? '--env-file' : '--env-file-if-exists'}=${nodeOptionsEnvFilePath}`,
`${i & 0b10 ? '--env-file' : '--env-file-if-exists'}=${validEnvFilePath}`,
'--eval', code,
],
{ cwd: __dirname },
)));
assert.deepStrictEqual(children, Array.from({ length: 4 }, () => ({
code: 0,
signal: null,
stdout: '',
stderr: '',
})));
});
it('supports absolute paths', async () => {
@ -39,6 +49,19 @@ describe('.env supports edge cases', () => {
assert.strictEqual(child.code, 0);
});
it('supports a space instead of \'=\' for the flag ', async () => {
const code = `
require('assert').strictEqual(process.env.BASIC, 'basic');
`.trim();
const child = await common.spawnPromisified(
process.execPath,
[ '--env-file', validEnvFilePath, '--eval', code ],
{ cwd: __dirname },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});
it('should handle non-existent .env file', async () => {
const code = `
require('assert').strictEqual(1, 1)
@ -48,10 +71,23 @@ describe('.env supports edge cases', () => {
[ '--env-file=.env', '--eval', code ],
{ cwd: __dirname },
);
assert.notStrictEqual(child.stderr.toString(), '');
assert.notStrictEqual(child.stderr, '');
assert.strictEqual(child.code, 9);
});
it('should handle non-existent optional .env file', async () => {
const code = `
require('assert').strictEqual(1,1);
`.trim();
const child = await common.spawnPromisified(
process.execPath,
['--env-file-if-exists=.env', '--eval', code],
{ cwd: __dirname },
);
assert.notStrictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});
it('should not override existing environment variables but introduce new vars', async () => {
const code = `
require('assert').strictEqual(process.env.BASIC, 'existing');
@ -106,7 +142,7 @@ describe('.env supports edge cases', () => {
'--eval', `require('assert').strictEqual(process.env.BASIC, undefined);`,
'--', '--env-file', validEnvFilePath,
],
{ cwd: fixtures.path('dotenv') },
{ cwd: __dirname },
);
assert.strictEqual(child.stdout, '');
assert.strictEqual(child.stderr, '');