mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
src: support multi-line values for .env file
PR-URL: https://github.com/nodejs/node/pull/51289 Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io> Reviewed-By: Zeyu "Alex" Yang <himself65@outlook.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Franziska Hinkelmann <franziska.hinkelmann@gmail.com>
This commit is contained in:
@@ -666,6 +666,10 @@ of `--enable-source-maps`.
|
||||
|
||||
<!-- YAML
|
||||
added: v20.6.0
|
||||
changes:
|
||||
- version: REPLACEME
|
||||
pr-url: https://github.com/nodejs/node/pull/51289
|
||||
description: Add support to multi-line values.
|
||||
-->
|
||||
|
||||
Loads environment variables from a file relative to the current directory,
|
||||
@@ -702,6 +706,20 @@ They are omitted from the values.
|
||||
USERNAME="nodejs" # will result in `nodejs` as the value.
|
||||
```
|
||||
|
||||
Multi-line values are supported:
|
||||
|
||||
```text
|
||||
MULTI_LINE="THIS IS
|
||||
A MULTILINE"
|
||||
# will result in `THIS IS\nA MULTILINE` as the value.
|
||||
```
|
||||
|
||||
Export keyword before a key is ignored:
|
||||
|
||||
```text
|
||||
export USERNAME="nodejs" # will result in `nodejs` as the value.
|
||||
```
|
||||
|
||||
### `-e`, `--eval "script"`
|
||||
|
||||
<!-- YAML
|
||||
|
||||
@@ -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"
|
||||
@@ -10,6 +12,15 @@ using v8::NewStringType;
|
||||
using v8::Object;
|
||||
using v8::String;
|
||||
|
||||
/**
|
||||
* The inspiration for this implementation comes from the original dotenv code,
|
||||
* available at https://github.com/motdotla/dotenv
|
||||
*/
|
||||
const std::regex LINE(
|
||||
"\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^']"
|
||||
")*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\r\n]+)?\\s*(?"
|
||||
":#.*)?"); // NOLINT(whitespace/line_length)
|
||||
|
||||
std::vector<std::string> Dotenv::GetPathFromArgs(
|
||||
const std::vector<std::string>& args) {
|
||||
const auto find_match = [](const std::string& arg) {
|
||||
@@ -91,11 +102,34 @@ Local<Object> Dotenv::ToObject(Environment* env) {
|
||||
}
|
||||
|
||||
void Dotenv::ParseContent(const std::string_view content) {
|
||||
using std::string_view_literals::operator""sv;
|
||||
auto lines = SplitString(content, "\n"sv);
|
||||
std::string lines = std::string(content);
|
||||
lines = std::regex_replace(lines, std::regex("\r\n?"), "\n");
|
||||
|
||||
for (const auto& line : lines) {
|
||||
ParseLine(line);
|
||||
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);
|
||||
|
||||
const char maybeQuote = value.front();
|
||||
|
||||
if (maybeQuote == '"') {
|
||||
value = std::regex_replace(value, std::regex("\\\\n"), "\n");
|
||||
value = std::regex_replace(value, std::regex("\\\\r"), "\r");
|
||||
}
|
||||
|
||||
// Remove surrounding quotes
|
||||
value = trim_quotes(value);
|
||||
|
||||
store_.insert_or_assign(std::string(key), value);
|
||||
lines = match.suffix();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,56 +179,13 @@ 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_view Dotenv::trim_quotes(std::string_view str) {
|
||||
static const std::unordered_set<char> quotes = {'"', '\'', '`'};
|
||||
if (str.size() >= 2 && quotes.count(str.front()) &&
|
||||
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(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
|
||||
|
||||
@@ -31,8 +31,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_view trim_quotes(std::string_view str);
|
||||
};
|
||||
|
||||
} // namespace node
|
||||
|
||||
25
test/fixtures/dotenv/valid.env
vendored
25
test/fixtures/dotenv/valid.env
vendored
@@ -20,6 +20,9 @@ BACKTICKS_SPACED=` backticks `
|
||||
DOUBLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" work inside backticks`
|
||||
SINGLE_QUOTES_INSIDE_BACKTICKS=`single 'quotes' work inside backticks`
|
||||
DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" and single 'quotes' work inside backticks`
|
||||
EXPAND_NEWLINES="expand\nnew\nlines"
|
||||
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
|
||||
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
|
||||
# COMMENTS=work
|
||||
INLINE_COMMENTS=inline comments # work #very #well
|
||||
INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
|
||||
@@ -34,3 +37,25 @@ TRIM_SPACE_FROM_UNQUOTED= some spaced out string
|
||||
EMAIL=therealnerdybeast@example.tld
|
||||
SPACED_KEY = parsed
|
||||
EDGE_CASE_INLINE_COMMENTS="VALUE1" # or "VALUE2" or "VALUE3"
|
||||
|
||||
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
|
||||
export EXAMPLE = ignore export
|
||||
|
||||
@@ -52,6 +52,8 @@ assert.strictEqual(process.env.INLINE_COMMENTS_DOUBLE_QUOTES, 'inline comments o
|
||||
assert.strictEqual(process.env.INLINE_COMMENTS_BACKTICKS, 'inline comments outside of #backticks');
|
||||
// Treats # character as start of comment
|
||||
assert.strictEqual(process.env.INLINE_COMMENTS_SPACE, 'inline comments start with a');
|
||||
// ignore comment
|
||||
assert.strictEqual(process.env.COMMENTS, undefined);
|
||||
// Respects equals signs in values
|
||||
assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==');
|
||||
// Retains inner quotes
|
||||
@@ -70,3 +72,15 @@ assert.strictEqual(process.env.EMAIL, 'therealnerdybeast@example.tld');
|
||||
assert.strictEqual(process.env.SPACED_KEY, 'parsed');
|
||||
// Parse inline comments correctly when multiple quotes
|
||||
assert.strictEqual(process.env.EDGE_CASE_INLINE_COMMENTS, 'VALUE1');
|
||||
// Test multi-line values with line breaks
|
||||
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');
|
||||
// Test that \n is expanded to a newline in double-quoted string
|
||||
assert.strictEqual(process.env.EXPAND_NEWLINES, 'expand\nnew\nlines');
|
||||
assert.strictEqual(process.env.DONT_EXPAND_UNQUOTED, 'dontexpand\\nnewlines');
|
||||
assert.strictEqual(process.env.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines');
|
||||
// Ignore export before key
|
||||
assert.strictEqual(process.env.EXAMPLE, 'ignore export');
|
||||
|
||||
@@ -17,23 +17,33 @@ const fs = require('node:fs');
|
||||
BACKTICKS_INSIDE_SINGLE: '`backticks` work inside single quotes',
|
||||
BACKTICKS_SPACED: ' backticks ',
|
||||
BASIC: 'basic',
|
||||
DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS: 'double "quotes" and single \'quotes\' work inside backticks',
|
||||
DONT_EXPAND_SQUOTED: 'dontexpand\\nnewlines',
|
||||
DONT_EXPAND_UNQUOTED: 'dontexpand\\nnewlines',
|
||||
DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS: "double \"quotes\" and single 'quotes' work inside backticks",
|
||||
DOUBLE_QUOTES: 'double_quotes',
|
||||
DOUBLE_QUOTES_INSIDE_BACKTICKS: 'double "quotes" work inside backticks',
|
||||
DOUBLE_QUOTES_INSIDE_SINGLE: 'double "quotes" work inside single quotes',
|
||||
DOUBLE_QUOTES_SPACED: ' double quotes ',
|
||||
DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET: '{ port: $MONGOLAB_PORT}',
|
||||
EDGE_CASE_INLINE_COMMENTS: 'VALUE1',
|
||||
EMAIL: 'therealnerdybeast@example.tld',
|
||||
EMPTY: '',
|
||||
EMPTY_BACKTICKS: '',
|
||||
EMPTY_DOUBLE_QUOTES: '',
|
||||
EMPTY_SINGLE_QUOTES: '',
|
||||
EQUAL_SIGNS: 'equals==',
|
||||
EXAMPLE: 'ignore export',
|
||||
EXPAND_NEWLINES: 'expand\nnew\nlines',
|
||||
INLINE_COMMENTS: 'inline comments',
|
||||
INLINE_COMMENTS_BACKTICKS: 'inline comments outside of #backticks',
|
||||
INLINE_COMMENTS_DOUBLE_QUOTES: 'inline comments outside of #doublequotes',
|
||||
INLINE_COMMENTS_SINGLE_QUOTES: 'inline comments outside of #singlequotes',
|
||||
INLINE_COMMENTS_SPACE: 'inline comments start with a',
|
||||
MULTI_BACKTICKED: 'THIS\nIS\nA\n"MULTILINE\'S"\nSTRING',
|
||||
MULTI_DOUBLE_QUOTED: 'THIS\nIS\nA\nMULTILINE\nSTRING',
|
||||
MULTI_NOT_VALID: 'THIS',
|
||||
MULTI_NOT_VALID_QUOTE: '"',
|
||||
MULTI_SINGLE_QUOTED: 'THIS\nIS\nA\nMULTILINE\nSTRING',
|
||||
RETAIN_INNER_QUOTES: '{"foo": "bar"}',
|
||||
RETAIN_INNER_QUOTES_AS_BACKTICKS: '{"foo": "bar\'s"}',
|
||||
RETAIN_INNER_QUOTES_AS_STRING: '{"foo": "bar"}',
|
||||
@@ -42,7 +52,7 @@ const fs = require('node:fs');
|
||||
SINGLE_QUOTES_INSIDE_DOUBLE: "single 'quotes' work inside double quotes",
|
||||
SINGLE_QUOTES_SPACED: ' single quotes ',
|
||||
SPACED_KEY: 'parsed',
|
||||
TRIM_SPACE_FROM_UNQUOTED: 'some spaced out string'
|
||||
TRIM_SPACE_FROM_UNQUOTED: 'some spaced out string',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user