Skip to content

Commit 99d1f88

Browse files
committed
src: support multi-line values for .env file
1 parent 89ddc98 commit 99d1f88

File tree

4 files changed

+69
-56
lines changed

4 files changed

+69
-56
lines changed

src/node_dotenv.cc

+41-55
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#include "node_dotenv.h"
2+
#include <regex> // NOLINT(build/c++11)
3+
#include <unordered_set>
24
#include "env-inl.h"
35
#include "node_file.h"
46
#include "uv.h"
@@ -8,6 +10,17 @@ namespace node {
810
using v8::NewStringType;
911
using v8::String;
1012

13+
/**
14+
* The inspiration for this implementation comes from the original dotenv code,
15+
* available at https://github.com/motdotla/dotenv
16+
*/
17+
18+
std::regex LINE(
19+
"(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|"
20+
"[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?"
21+
"\\s*(?:#.*)?(?:$|$)",
22+
std::regex_constants::multiline);
23+
1124
std::vector<std::string> Dotenv::GetPathFromArgs(
1225
const std::vector<std::string>& args) {
1326
const auto find_match = [](const std::string& arg) {
@@ -81,7 +94,7 @@ bool Dotenv::ParsePath(const std::string_view path) {
8194
uv_fs_req_cleanup(&close_req);
8295
});
8396

84-
std::string result{};
97+
std::string lines{};
8598
char buffer[8192];
8699
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));
87100

@@ -95,15 +108,32 @@ bool Dotenv::ParsePath(const std::string_view path) {
95108
if (r <= 0) {
96109
break;
97110
}
98-
result.append(buf.base, r);
111+
lines.append(buf.base, r);
99112
}
100113

101-
using std::string_view_literals::operator""sv;
102-
auto lines = SplitString(result, "\n"sv);
114+
// Convert line breaks to the same format
115+
std::regex_replace(lines, std::regex("\r\n?"), "\n");
116+
117+
std::smatch match;
118+
while (std::regex_search(lines, match, LINE)) {
119+
const std::string key = match[1].str();
120+
121+
// Default undefined or null to an empty string
122+
std::string value = match[2].str();
123+
124+
// Remove leading whitespaces
125+
value.erase(0, value.find_first_not_of(" \t"));
126+
127+
// Remove trailing whitespaces
128+
value.erase(value.find_last_not_of(" \t") + 1);
129+
130+
// Remove surrounding quotes
131+
value = trim_quotes(value);
103132

104-
for (const auto& line : lines) {
105-
ParseLine(line);
133+
store_.insert_or_assign(std::string(key), value);
134+
lines = match.suffix();
106135
}
136+
107137
return true;
108138
}
109139

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

118-
void Dotenv::ParseLine(const std::string_view line) {
119-
auto equal_index = line.find('=');
120-
121-
if (equal_index == std::string_view::npos) {
122-
return;
148+
std::string Dotenv::trim_quotes(std::string str) {
149+
static const std::unordered_set<char> quotes = {'"', '\'', '`'};
150+
if (str.size() >= 2 && quotes.count(str[0]) && quotes.count(str.back())) {
151+
str = str.substr(1, str.size() - 2);
123152
}
124-
125-
auto key = line.substr(0, equal_index);
126-
127-
// Remove leading and trailing space characters from key.
128-
while (!key.empty() && std::isspace(key.front())) key.remove_prefix(1);
129-
while (!key.empty() && std::isspace(key.back())) key.remove_suffix(1);
130-
131-
// Omit lines with comments
132-
if (key.front() == '#' || key.empty()) {
133-
return;
134-
}
135-
136-
auto value = std::string(line.substr(equal_index + 1));
137-
138-
// Might start and end with `"' characters.
139-
auto quotation_index = value.find_first_of("`\"'");
140-
141-
if (quotation_index == 0) {
142-
auto quote_character = value[quotation_index];
143-
value.erase(0, 1);
144-
145-
auto end_quotation_index = value.find_last_of(quote_character);
146-
147-
// We couldn't find the closing quotation character. Terminate.
148-
if (end_quotation_index == std::string::npos) {
149-
return;
150-
}
151-
152-
value.erase(end_quotation_index);
153-
} else {
154-
auto hash_index = value.find('#');
155-
156-
// Remove any inline comments
157-
if (hash_index != std::string::npos) {
158-
value.erase(hash_index);
159-
}
160-
161-
// Remove any leading/trailing spaces from unquoted values.
162-
while (!value.empty() && std::isspace(value.front())) value.erase(0, 1);
163-
while (!value.empty() && std::isspace(value.back()))
164-
value.erase(value.size() - 1);
165-
}
166-
167-
store_.insert_or_assign(std::string(key), value);
153+
return str;
168154
}
169155

170156
} // namespace node

src/node_dotenv.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ class Dotenv {
2626
const std::vector<std::string>& args);
2727

2828
private:
29-
void ParseLine(const std::string_view line);
3029
std::map<std::string, std::string> store_;
30+
std::string trim_quotes(std::string str);
3131
};
3232

3333
} // namespace node

test/fixtures/dotenv/valid.env

+21
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,24 @@ RETAIN_INNER_QUOTES_AS_BACKTICKS=`{"foo": "bar's"}`
3333
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
3434
EMAIL=therealnerdybeast@example.tld
3535
SPACED_KEY = parsed
36+
37+
MULTI_DOUBLE_QUOTED="THIS
38+
IS
39+
A
40+
MULTILINE
41+
STRING"
42+
43+
MULTI_SINGLE_QUOTED='THIS
44+
IS
45+
A
46+
MULTILINE
47+
STRING'
48+
49+
MULTI_BACKTICKED=`THIS
50+
IS
51+
A
52+
"MULTILINE'S"
53+
STRING`
54+
MULTI_NOT_VALID_QUOTE="
55+
MULTI_NOT_VALID=THIS
56+
IS NOT MULTILINE

test/parallel/test-dotenv.js

+6
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,9 @@ assert.strictEqual(process.env.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string
6868
assert.strictEqual(process.env.EMAIL, 'therealnerdybeast@example.tld');
6969
// Parses keys and values surrounded by spaces
7070
assert.strictEqual(process.env.SPACED_KEY, 'parsed');
71+
// Test multiple-line value
72+
assert.strictEqual(process.env.MULTI_DOUBLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING');
73+
assert.strictEqual(process.env.MULTI_SINGLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING');
74+
assert.strictEqual(process.env.MULTI_BACKTICKED, 'THIS\nIS\nA\n"MULTILINE\'S"\nSTRING');
75+
assert.strictEqual(process.env.MULTI_NOT_VALID_QUOTE, '"');
76+
assert.strictEqual(process.env.MULTI_NOT_VALID, 'THIS');

0 commit comments

Comments
 (0)