Skip to content

Commit

Permalink
src: support multi-line values for .env file
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyasShabiCS committed Dec 31, 2023
1 parent 89ddc98 commit e7fe39d
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 56 deletions.
96 changes: 41 additions & 55 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "node_dotenv.h"
#include <regex> // NOLINT(build/c++11)
#include <unordered_set>
#include "env-inl.h"
#include "node_file.h"
#include "uv.h"
Expand All @@ -8,6 +10,17 @@ namespace node {
using v8::NewStringType;
using v8::String;

/**
* The inspiration for this implementation comes from the original dotenv code,
* available at https://github.com/motdotla/dotenv
*/

std::regex LINE(
"(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|"
"[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?"
"\\s*(?:#.*)?(?:$|$)",
std::regex::ECMAScript | std::regex::multiline);

std::vector<std::string> Dotenv::GetPathFromArgs(
const std::vector<std::string>& args) {
const auto find_match = [](const std::string& arg) {
Expand Down Expand Up @@ -81,7 +94,7 @@ bool Dotenv::ParsePath(const std::string_view path) {
uv_fs_req_cleanup(&close_req);
});

std::string result{};
std::string lines{};
char buffer[8192];
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));

Expand All @@ -95,15 +108,32 @@ bool Dotenv::ParsePath(const std::string_view path) {
if (r <= 0) {
break;
}
result.append(buf.base, r);
lines.append(buf.base, r);
}

using std::string_view_literals::operator""sv;
auto lines = SplitString(result, "\n"sv);
// Convert line breaks to the same format
std::regex_replace(lines, std::regex("\r\n?"), "\n");

std::smatch match;
while (std::regex_search(lines, match, LINE)) {
const std::string key = match[1].str();

// Default undefined or null to an empty string
std::string value = match[2].str();

// Remove leading whitespaces
value.erase(0, value.find_first_not_of(" \t"));

// Remove trailing whitespaces
value.erase(value.find_last_not_of(" \t") + 1);

// Remove surrounding quotes
value = trim_quotes(value);

for (const auto& line : lines) {
ParseLine(line);
store_.insert_or_assign(std::string(key), value);
lines = match.suffix();
}

return true;
}

Expand All @@ -115,56 +145,12 @@ void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {
}
}

void Dotenv::ParseLine(const std::string_view line) {
auto equal_index = line.find('=');

if (equal_index == std::string_view::npos) {
return;
std::string Dotenv::trim_quotes(std::string str) {
static const std::unordered_set<char> quotes = {'"', '\'', '`'};
if (str.size() >= 2 && quotes.count(str[0]) && quotes.count(str.back())) {
str = str.substr(1, str.size() - 2);
}

auto key = line.substr(0, equal_index);

// Remove leading and trailing space characters from key.
while (!key.empty() && std::isspace(key.front())) key.remove_prefix(1);
while (!key.empty() && std::isspace(key.back())) key.remove_suffix(1);

// Omit lines with comments
if (key.front() == '#' || key.empty()) {
return;
}

auto value = std::string(line.substr(equal_index + 1));

// Might start and end with `"' characters.
auto quotation_index = value.find_first_of("`\"'");

if (quotation_index == 0) {
auto quote_character = value[quotation_index];
value.erase(0, 1);

auto end_quotation_index = value.find_last_of(quote_character);

// We couldn't find the closing quotation character. Terminate.
if (end_quotation_index == std::string::npos) {
return;
}

value.erase(end_quotation_index);
} else {
auto hash_index = value.find('#');

// Remove any inline comments
if (hash_index != std::string::npos) {
value.erase(hash_index);
}

// Remove any leading/trailing spaces from unquoted values.
while (!value.empty() && std::isspace(value.front())) value.erase(0, 1);
while (!value.empty() && std::isspace(value.back()))
value.erase(value.size() - 1);
}

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

} // namespace node
2 changes: 1 addition & 1 deletion src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class Dotenv {
const std::vector<std::string>& args);

private:
void ParseLine(const std::string_view line);
std::map<std::string, std::string> store_;
std::string trim_quotes(std::string str);
};

} // namespace node
Expand Down
21 changes: 21 additions & 0 deletions test/fixtures/dotenv/valid.env
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,24 @@ RETAIN_INNER_QUOTES_AS_BACKTICKS=`{"foo": "bar's"}`
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
EMAIL=therealnerdybeast@example.tld
SPACED_KEY = parsed

MULTI_DOUBLE_QUOTED="THIS
IS
A
MULTILINE
STRING"

MULTI_SINGLE_QUOTED='THIS
IS
A
MULTILINE
STRING'

MULTI_BACKTICKED=`THIS
IS
A
"MULTILINE'S"
STRING`
MULTI_NOT_VALID_QUOTE="
MULTI_NOT_VALID=THIS
IS NOT MULTILINE
6 changes: 6 additions & 0 deletions test/parallel/test-dotenv.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ assert.strictEqual(process.env.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string
assert.strictEqual(process.env.EMAIL, 'therealnerdybeast@example.tld');
// Parses keys and values surrounded by spaces
assert.strictEqual(process.env.SPACED_KEY, 'parsed');
// Test multiple-line value
assert.strictEqual(process.env.MULTI_DOUBLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING');
assert.strictEqual(process.env.MULTI_SINGLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING');
assert.strictEqual(process.env.MULTI_BACKTICKED, 'THIS\nIS\nA\n"MULTILINE\'S"\nSTRING');
assert.strictEqual(process.env.MULTI_NOT_VALID_QUOTE, '"');
assert.strictEqual(process.env.MULTI_NOT_VALID, 'THIS');

0 comments on commit e7fe39d

Please sign in to comment.