Skip to content

Commit

Permalink
src: support multiple --env-file declarations
Browse files Browse the repository at this point in the history
PR-URL: nodejs#49542
Refs: nodejs#49148
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
  • Loading branch information
anonrig authored and alexfernandez committed Nov 1, 2023
1 parent a5cc5d5 commit ff715fe
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 36 deletions.
7 changes: 7 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,13 @@ variables which configure Node.js][environment_variables], such as `NODE_OPTIONS
are parsed and applied. If the same variable is defined in the environment and
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.

```bash
node --env-file=.env --env-file=.development.env index.js
```

The format of the file should be one line per key-value pair of environment
variable name and value separated by `=`:

Expand Down
14 changes: 9 additions & 5 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -841,13 +841,17 @@ static ExitCode InitializeNodeWithArgsInternal(
HandleEnvOptions(per_process::cli_options->per_isolate->per_env);

std::string node_options;
auto file_path = node::Dotenv::GetPathFromArgs(*argv);
auto file_paths = node::Dotenv::GetPathFromArgs(*argv);

if (file_path.has_value()) {
auto cwd = Environment::GetCwd(Environment::GetExecPath(*argv));
std::string path = cwd + kPathSeparator + file_path.value();
if (!file_paths.empty()) {
CHECK(!per_process::v8_initialized);
per_process::dotenv_file.ParsePath(path);
auto cwd = Environment::GetCwd(Environment::GetExecPath(*argv));

for (const auto& file_path : file_paths) {
std::string path = cwd + kPathSeparator + file_path;
per_process::dotenv_file.ParsePath(path);
}

per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
}

Expand Down
50 changes: 25 additions & 25 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,34 @@ namespace node {
using v8::NewStringType;
using v8::String;

std::optional<std::string> Dotenv::GetPathFromArgs(
std::vector<std::string> Dotenv::GetPathFromArgs(
const std::vector<std::string>& args) {
std::string_view flag = "--env-file";
// Match the last `--env-file`
// This is required to imitate the default behavior of Node.js CLI argument
// matching.
auto path =
std::find_if(args.rbegin(), args.rend(), [&flag](const std::string& arg) {
return strncmp(arg.c_str(), flag.data(), flag.size()) == 0;
});

if (path == args.rend()) {
return std::nullopt;
}

auto equal_char = path->find('=');

if (equal_char != std::string::npos) {
return path->substr(equal_char + 1);
}

auto next_arg = std::prev(path);
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;
};
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('=');

if (equal_char != std::string::npos) {
paths.push_back(path->substr(equal_char + 1));
} else {
auto next_path = std::next(path);

if (next_path == args.end()) {
return paths;
}

paths.push_back(*next_path);
}

if (next_arg == args.rend()) {
return std::nullopt;
path = std::find_if(++path, args.end(), find_match);
}

return *next_arg;
return paths;
}

void Dotenv::SetEnvironment(node::Environment* env) {
Expand Down Expand Up @@ -163,7 +163,7 @@ void Dotenv::ParseLine(const std::string_view line) {
value.erase(value.size() - 1);
}

store_.emplace(key, value);
store_.insert_or_assign(std::string(key), value);
}

} // namespace node
3 changes: 1 addition & 2 deletions src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#include "util-inl.h"

#include <map>
#include <optional>

namespace node {

Expand All @@ -23,7 +22,7 @@ class Dotenv {
void AssignNodeOptionsIfAvailable(std::string* node_options);
void SetEnvironment(Environment* env);

static std::optional<std::string> GetPathFromArgs(
static std::vector<std::string> GetPathFromArgs(
const std::vector<std::string>& args);

private:
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/dotenv/node-options.env
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ NODE_NO_WARNINGS=1
NODE_OPTIONS="--experimental-permission --allow-fs-read=*"
TZ=Pacific/Honolulu
UV_THREADPOOL_SIZE=5
BASIC=overridden
11 changes: 7 additions & 4 deletions test/parallel/test-dotenv-edge-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ const assert = require('node:assert');
const { describe, it } = require('node:test');

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

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

it('should use the last --env-file declaration', async () => {
it('supports multiple declarations', async () => {
// process.env.BASIC is equal to `basic` because the second .env file overrides it.
const code = `
require('assert').strictEqual(process.env.BASIC, 'basic');
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=${relativePath}`, `--env-file=${validEnvFilePath}`, '--eval', code ],
[ `--env-file=${nodeOptionsEnvFilePath}`, `--env-file=${validEnvFilePath}`, '--eval', code ],
{ cwd: __dirname },
);
assert.strictEqual(child.stderr, '');
Expand Down

0 comments on commit ff715fe

Please sign in to comment.