Skip to content

Commit 5a81130

Browse files
author
Luboš Matejčík
committed
dotenv: fix env-file parsing
Implementation of parsing is identical to dotenv npm package
1 parent e2b7e41 commit 5a81130

File tree

1 file changed

+38
-116
lines changed

1 file changed

+38
-116
lines changed

src/node_dotenv.cc

+38-116
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "node_dotenv.h"
2+
#include <regex>
23
#include <unordered_set>
34
#include "env-inl.h"
45
#include "node_file.h"
@@ -87,137 +88,58 @@ Local<Object> Dotenv::ToObject(Environment* env) const {
8788
return result;
8889
}
8990

90-
std::string_view trim_spaces(std::string_view input) {
91-
if (input.empty()) return "";
92-
if (input.front() == ' ') {
93-
input.remove_prefix(input.find_first_not_of(' '));
94-
}
95-
if (!input.empty() && input.back() == ' ') {
96-
input = input.substr(0, input.find_last_not_of(' ') + 1);
97-
}
98-
return input;
91+
const std::regex whitespace_regex{
92+
"(^\\s+|\\s+$)",
93+
std::regex_constants::ECMAScript | std::regex_constants::multiline};
94+
95+
std::string trim_spaces(const std::string& input) {
96+
return std::regex_replace(input, whitespace_regex, "");
9997
}
10098

99+
const std::regex env_regex{
100+
R"((?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$))",
101+
std::regex_constants::ECMAScript | std::regex_constants::multiline};
102+
103+
const std::regex quotes_regex{
104+
R"(^(['"`])([\s\S]*)\1$)",
105+
std::regex_constants::ECMAScript | std::regex_constants::multiline};
106+
const std::regex newline_regex{
107+
R"(\\n)",
108+
std::regex_constants::ECMAScript | std::regex_constants::multiline};
109+
const std::regex cr_regex{
110+
R"(\\r)",
111+
std::regex_constants::ECMAScript | std::regex_constants::multiline};
112+
101113
void Dotenv::ParseContent(const std::string_view input) {
102114
std::string lines(input);
103115

104116
// Handle windows newlines "\r\n": remove "\r" and keep only "\n"
105117
lines.erase(std::remove(lines.begin(), lines.end(), '\r'), lines.end());
106118

107-
std::string_view content = lines;
108-
content = trim_spaces(content);
109-
110-
std::string_view key;
111-
std::string_view value;
119+
std::string content = trim_spaces(lines);
112120

113-
while (!content.empty()) {
114-
// Skip empty lines and comments
115-
if (content.front() == '\n' || content.front() == '#') {
116-
auto newline = content.find('\n');
117-
if (newline != std::string_view::npos) {
118-
content.remove_prefix(newline + 1);
119-
continue;
120-
}
121-
}
121+
std::string key;
122+
std::string value;
122123

123-
// If there is no equal character, then ignore everything
124-
auto equal = content.find('=');
125-
if (equal == std::string_view::npos) {
126-
break;
127-
}
124+
auto matches_begin =
125+
std::sregex_iterator(content.cbegin(), content.cend(), env_regex);
126+
auto matches_end = std::sregex_iterator();
128127

129-
key = content.substr(0, equal);
130-
content.remove_prefix(equal + 1);
131-
key = trim_spaces(key);
132-
content = trim_spaces(content);
128+
for (std::regex_iterator i = matches_begin; i != matches_end; ++i) {
129+
key = i->str(1);
130+
value = i->str(2);
131+
value = trim_spaces(value);
133132

134-
if (key.empty()) {
135-
break;
136-
}
133+
const auto has_opening_double_quote = value.size() > 0 && value[0] == '\"';
134+
value = std::regex_replace(value, quotes_regex, "$2");
137135

138-
// Remove export prefix from key
139-
if (key.starts_with("export ")) {
140-
key.remove_prefix(7);
136+
if (has_opening_double_quote) {
137+
// replace escaped newlines and carriage returns
138+
value = std::regex_replace(value, newline_regex, "\n");
139+
value = std::regex_replace(value, cr_regex, "\r");
141140
}
142141

143-
// SAFETY: Content is guaranteed to have at least one character
144-
if (content.empty()) {
145-
// In case the last line is a single key without value
146-
// Example: KEY= (without a newline at the EOF)
147-
store_.insert_or_assign(std::string(key), "");
148-
break;
149-
}
150-
151-
// Expand new line if \n it's inside double quotes
152-
// Example: EXPAND_NEWLINES = 'expand\nnew\nlines'
153-
if (content.front() == '"') {
154-
auto closing_quote = content.find(content.front(), 1);
155-
if (closing_quote != std::string_view::npos) {
156-
value = content.substr(1, closing_quote - 1);
157-
std::string multi_line_value = std::string(value);
158-
159-
size_t pos = 0;
160-
while ((pos = multi_line_value.find("\\n", pos)) !=
161-
std::string_view::npos) {
162-
multi_line_value.replace(pos, 2, "\n");
163-
pos += 1;
164-
}
165-
166-
store_.insert_or_assign(std::string(key), multi_line_value);
167-
content.remove_prefix(content.find('\n', closing_quote + 1));
168-
continue;
169-
}
170-
}
171-
172-
// Check if the value is wrapped in quotes, single quotes or backticks
173-
if ((content.front() == '\'' || content.front() == '"' ||
174-
content.front() == '`')) {
175-
auto closing_quote = content.find(content.front(), 1);
176-
177-
// Check if the closing quote is not found
178-
// Example: KEY="value
179-
if (closing_quote == std::string_view::npos) {
180-
// Check if newline exist. If it does, take the entire line as the value
181-
// Example: KEY="value\nKEY2=value2
182-
// The value pair should be `"value`
183-
auto newline = content.find('\n');
184-
if (newline != std::string_view::npos) {
185-
value = content.substr(0, newline);
186-
store_.insert_or_assign(std::string(key), value);
187-
content.remove_prefix(newline);
188-
}
189-
} else {
190-
// Example: KEY="value"
191-
value = content.substr(1, closing_quote - 1);
192-
store_.insert_or_assign(std::string(key), value);
193-
// Select the first newline after the closing quotation mark
194-
// since there could be newline characters inside the value.
195-
content.remove_prefix(content.find('\n', closing_quote + 1));
196-
}
197-
} else {
198-
// Regular key value pair.
199-
// Example: `KEY=this is value`
200-
auto newline = content.find('\n');
201-
202-
if (newline != std::string_view::npos) {
203-
value = content.substr(0, newline);
204-
auto hash_character = value.find('#');
205-
// Check if there is a comment in the line
206-
// Example: KEY=value # comment
207-
// The value pair should be `value`
208-
if (hash_character != std::string_view::npos) {
209-
value = content.substr(0, hash_character);
210-
}
211-
content.remove_prefix(newline);
212-
} else {
213-
// In case the last line is a single key/value pair
214-
// Example: KEY=VALUE (without a newline at the EOF)
215-
value = content.substr(0);
216-
}
217-
218-
value = trim_spaces(value);
219-
store_.insert_or_assign(std::string(key), value);
220-
}
142+
store_.insert_or_assign(key, value);
221143
}
222144
}
223145

0 commit comments

Comments
 (0)