Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix multiline value parsing regressions reported in v2.3.0 #346

Merged
merged 10 commits into from
Jun 14, 2018
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,21 @@ export SECRET_KEY=YOURSECRETKEYGOESHERE
If you need multiline variables, for example private keys, you can double quote strings and use the `\n` character for newlines:

```shell
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9\n-----END DSA PRIVATE KEY-----\n"
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9...\n-----END DSA PRIVATE KEY-----\n"
```

Alternatively, multi-line values with line breaks are now supported for quoted values.

```shell
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...
HkVN9...
...
-----END DSA PRIVATE KEY-----"
```

This is particularly helpful when using the Heroku command line plugin [`heroku-config`](https://github.com/xavdid/heroku-config) to pull configuration variables down that may have line breaks.

### Command Substitution

You need to add the output of a command in one of your variables? Simply add it with `$(your_command)`:
Expand Down
46 changes: 25 additions & 21 deletions lib/dotenv/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,21 @@ class Parser
[Dotenv::Substitutions::Variable, Dotenv::Substitutions::Command]

LINE = /
\A
\s*
(?:export\s+)? # optional export
([\w\.]+) # key
(?:\s*=\s*|:\s+?) # separator
( # optional value begin
'(?:\'|[^'])*' # single quoted value
| # or
"(?:\"|[^"])*" # double quoted value
| # or
[^#\n]+ # unquoted value
)? # value end
\s*
(?:\#.*)? # optional comment
\z
(?:^|\A) # beginning of line
\s* # leading whitespace
(?:export\s+)? # optional export
([\w\.]+) # key
(?:\s*=\s*?|:\s+?) # separator
( # optional value begin
'(?:\\'|[^'])*' # single quoted value
| # or
"(?:\\"|[^"])*" # double quoted value
| # or
[^\#\r\n]+ # unquoted value
)? # value end
\s* # trailing whitespace
(?:\#.*)? # optional comment
(?:$|\z) # end of line
/x

class << self
Expand All @@ -44,7 +44,14 @@ def initialize(string, is_load = false)
end

def call
@string.split(/[\n\r]+/).each do |line|
# Convert line breaks to same format
lines = @string.gsub(/\r\n?/, "\n")
# Process matches
lines.scan(LINE).each do |key, value|
@hash[key] = parse_value(value || "")
end
# Process non-matches
lines.gsub(LINE, "").split(/[\n\r]+/).each do |line|
parse_line(line)
end
@hash
Expand All @@ -53,10 +60,7 @@ def call
private

def parse_line(line)
if (match = line.match(LINE))
key, value = match.captures
@hash[key] = parse_value(value || "")
elsif line.split.first == "export"
if line.split.first == "export"
if variable_not_set?(line)
raise FormatError, "Line #{line.inspect} has an unset variable"
end
Expand All @@ -65,7 +69,7 @@ def parse_line(line)

def parse_value(value)
# Remove surrounding quotes
value = value.strip.sub(/\A(['"])(.*)\1\z/, '\2')
value = value.strip.sub(/\A(['"])(.*)\1\z/m, '\2')

if Regexp.last_match(1) == '"'
value = unescape_characters(expand_newlines(value))
Expand Down
52 changes: 52 additions & 0 deletions spec/dotenv/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@ def env(string)
expect(env("# Uncomment to activate:\n")).to eql({})
end

it "includes variables without values" do
input = 'DATABASE_PASSWORD=
DATABASE_USERNAME=root
DATABASE_HOST=/tmp/mysql.sock'

output = {
"DATABASE_PASSWORD" => "",
"DATABASE_USERNAME" => "root",
"DATABASE_HOST" => "/tmp/mysql.sock"
}

expect(env(input)).to eql(output)
end

it "parses # in quoted values" do
expect(env('foo="ba#r"')).to eql("foo" => "ba#r")
expect(env("foo='ba#r'")).to eql("foo" => "ba#r")
Expand All @@ -169,6 +183,44 @@ def env(string)
expect(env("foo=")).to eql("foo" => "")
end

it "allows multi-line values in single quotes" do
env_file = %(OPTION_A=first line
export OPTION_B='line 1
line 2
line 3'
OPTION_C="last line"
OPTION_ESCAPED='line one
this is \\'quoted\\'
one more line')

expected_result = {
"OPTION_A" => "first line",
"OPTION_B" => "line 1\nline 2\nline 3",
"OPTION_C" => "last line",
"OPTION_ESCAPED" => "line one\nthis is \\'quoted\\'\none more line"
}
expect(env(env_file)).to eql(expected_result)
end

it "allows multi-line values in double quotes" do
env_file = %(OPTION_A=first line
export OPTION_B="line 1
line 2
line 3"
OPTION_C="last line"
OPTION_ESCAPED="line one
this is \\"quoted\\"
one more line")

expected_result = {
"OPTION_A" => "first line",
"OPTION_B" => "line 1\nline 2\nline 3",
"OPTION_C" => "last line",
"OPTION_ESCAPED" => "line one\nthis is \"quoted\"\none more line"
}
expect(env(env_file)).to eql(expected_result)
end

if RUBY_VERSION > "1.8.7"
it "parses shell commands interpolated in $()" do
expect(env("echo=$(echo hello)")).to eql("echo" => "hello")
Expand Down