Skip to content

Commit

Permalink
src: add --optional-env-file flag
Browse files Browse the repository at this point in the history
  • Loading branch information
BoscoDomingo committed May 19, 2024
1 parent a6d54f1 commit 9e6fa7a
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 21 deletions.
11 changes: 11 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,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
```
Expand Down Expand Up @@ -771,6 +773,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 does not exist, you
can use the [`--optional-env-file`](#optional-env-fileconfig) flag instead.

### `-e`, `--eval "script"`

<!-- YAML
Expand Down Expand Up @@ -1554,6 +1559,12 @@ 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.

### `--optional-env-file=config`

Behaviour is the same as [`--env-file`](#env-fileconfig), but an error is not thrown if the file
does not exist.


### `--pending-deprecation`

<!-- YAML
Expand Down
13 changes: 7 additions & 6 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -832,20 +832,21 @@ 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::GetEnvFileDataFromArgs(*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_required) continue;
errors->push_back(file_data.path + ": not found");
break;
default:
UNREACHABLE();
Expand Down
45 changes: 31 additions & 14 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,51 @@ using v8::NewStringType;
using v8::Object;
using v8::String;

std::vector<std::string> Dotenv::GetPathFromArgs(
std::vector<Dotenv::EnvFileData> Dotenv::GetEnvFileDataFromArgs(
const std::vector<std::string>& args) {
const std::string_view env_file_flag = "--env-file";

const auto find_match = [](const std::string& arg) {
const std::string_view flag = "--env-file";
return strncmp(arg.c_str(), flag.data(), flag.size()) == 0;
const std::string_view env_file_flag = "--env-file";
const std::string_view optional_env_file_flag = "--optional-env-file";
return strncmp(arg.c_str(), env_file_flag.data(), env_file_flag.size()) == 0 ||
strncmp(arg.c_str(),
optional_env_file_flag.data(),
optional_env_file_flag.size()) == 0;
};
std::vector<std::string> paths;
auto path = std::find_if(args.begin(), args.end(), find_match);

while (path != args.end()) {
auto equal_char = path->find('=');
std::vector<Dotenv::EnvFileData> 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()) {
auto equal_char = matched_arg->find('=');

if (equal_char != std::string::npos) {
paths.push_back(path->substr(equal_char + 1));
auto flag = matched_arg->substr(0, equal_char);
struct EnvFileData env_file_data = {
matched_arg->substr(equal_char + 1),
strncmp(flag.c_str(), env_file_flag.data(), env_file_flag.size()) == 0
};
env_files.push_back(env_file_data);
} else {
auto next_path = std::next(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 EnvFileData env_file_data = {
*file_path,
strncmp(matched_arg->c_str(), env_file_flag.data(), env_file_flag.size()) == 0
};
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) {
Expand Down
6 changes: 5 additions & 1 deletion src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ namespace node {
class Dotenv {
public:
enum ParseResult { Valid, FileError, InvalidContent };
struct EnvFileData {
std::string path;
bool is_required;
};

Dotenv() = default;
Dotenv(const Dotenv& d) = delete;
Expand All @@ -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<EnvFileData> GetEnvFileDataFromArgs(
const std::vector<std::string>& args);

private:
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"set environment variables from supplied file",
&EnvironmentOptions::env_file);
Implies("--env-file", "[has_env_file_string]");
AddOption("--optional-env-file",
"set environment variables from supplied file, but won't fail if the file doesn't exist",
&EnvironmentOptions::optional_env_file);
Implies("--optional-env-file", "[has_env_file_string]");
AddOption("--test",
"launch test runner on startup",
&EnvironmentOptions::test_runner);
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,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;
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/dotenv/optional.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
BASIC="OPTIONALLY LOADED"
30 changes: 30 additions & 0 deletions test/parallel/test-dotenv-edge-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const fixtures = require('../common/fixtures');

const validEnvFilePath = '../fixtures/dotenv/valid.env';
const nodeOptionsEnvFilePath = '../fixtures/dotenv/node-options.env';
const optionalEnvFilePath = '../fixtures/dotenv/optional.env';

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

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

it('supports multiple declarations, including optional ones', async () => {
// process.env.BASIC is equal to `OPTIONALLY LOADED` because the third .env file overrides it.
const code = `
const assert = require('assert');
assert.strictEqual(process.env.BASIC, 'OPTIONALLY LOADED');
assert.strictEqual(process.env.NODE_NO_WARNINGS, '1');
`.trim();
const child = await common.spawnPromisified(
process.execPath,
[ `--env-file=${nodeOptionsEnvFilePath}`, `--optional-env-file=${optionalEnvFilePath}`, '--eval', code ],
{ cwd: __dirname },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});

it('supports absolute paths', async () => {
const code = `
require('assert').strictEqual(process.env.BASIC, 'basic');
Expand All @@ -52,6 +69,19 @@ describe('.env supports edge cases', () => {
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,
[ '--optional-env-file=.env', '--eval', code ],
{ cwd: __dirname },
);
assert.strictEqual(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');
Expand Down

0 comments on commit 9e6fa7a

Please sign in to comment.