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 load to respect existing environment variables over env file when doing variable interpolation #323

Merged
merged 3 commits into from
Apr 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/dotenv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class << self
def load(*filenames)
with(*filenames) do |f|
ignoring_nonexistent_files do
env = Environment.new(f)
env = Environment.new(f, true)
instrument("dotenv.load", env: env) { env.apply }
end
end
Expand All @@ -21,7 +21,7 @@ def load(*filenames)
# same as `load`, but raises Errno::ENOENT if any files don't exist
def load!(*filenames)
with(*filenames) do |f|
env = Environment.new(f)
env = Environment.new(f, true)
instrument("dotenv.load", env: env) { env.apply }
end
end
Expand All @@ -30,7 +30,7 @@ def load!(*filenames)
def overload(*filenames)
with(*filenames) do |f|
ignoring_nonexistent_files do
env = Environment.new(f)
env = Environment.new(f, false)
instrument("dotenv.overload", env: env) { env.apply! }
end
end
Expand Down
8 changes: 4 additions & 4 deletions lib/dotenv/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ module Dotenv
class Environment < Hash
attr_reader :filename

def initialize(filename)
def initialize(filename, is_load)
@filename = filename
load
load(is_load)
end

def load
update Parser.call(read)
def load(is_load)
update Parser.call(read, is_load)
end

def read
Expand Down
9 changes: 5 additions & 4 deletions lib/dotenv/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ class Parser
class << self
attr_reader :substitutions

def call(string)
new(string).call
def call(string, is_load)
new(string, is_load).call
end
end

def initialize(string)
def initialize(string, is_load)
@string = string
@hash = {}
@is_load = is_load
end

def call
Expand Down Expand Up @@ -72,7 +73,7 @@ def parse_value(value)

if Regexp.last_match(1) != "'"
self.class.substitutions.each do |proc|
value = proc.call(value, @hash)
value = proc.call(value, @hash, @is_load)
end
end
value
Expand Down
2 changes: 1 addition & 1 deletion lib/dotenv/substitutions/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class << self
)
/x

def call(value, _env)
def call(value, _env, _is_load)
# Process interpolated shell commands
value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
# Eliminate opening and closing parentheses
Expand Down
26 changes: 18 additions & 8 deletions lib/dotenv/substitutions/variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,27 @@ class << self
\}? # closing brace
/xi

def call(value, env)
def call(value, env, is_load)
combined_env = if is_load
env.merge(ENV)
else
ENV.to_h.merge(env)
end
value.gsub(VARIABLE) do |variable|
match = $LAST_MATCH_INFO
substitute(match, variable, combined_env)
end
end

private

if match[1] == '\\'
variable[1..-1]
elsif match[3]
ENV[match[3]] || env[match[3]]
else
variable
end
def substitute(match, variable, env)
if match[1] == '\\'
variable[1..-1]
elsif match[3]
env.fetch(match[3], "")
else
variable
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/dotenv/environment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

it "fails if file does not exist" do
expect do
Dotenv::Environment.new(".does_not_exists")
Dotenv::Environment.new(".does_not_exists", true)
end.to raise_error(Errno::ENOENT)
end
end
Expand Down Expand Up @@ -47,7 +47,7 @@ def env(text)
file = Tempfile.new("dotenv")
file.write text
file.close
env = Dotenv::Environment.new(file.path)
env = Dotenv::Environment.new(file.path, true)
file.unlink
env
end
Expand Down
19 changes: 13 additions & 6 deletions spec/dotenv/parser_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

describe Dotenv::Parser do
def env(string)
Dotenv::Parser.call(string)
Dotenv::Parser.call(string, true)
end

it "parses unquoted values" do
Expand Down Expand Up @@ -55,14 +55,21 @@ def env(string)
.to eql("FOO" => "test", "BAR" => "testbar")
end

it "reads variable from ENV when expanding first" do
ENV["FOO"] = "bar"
expect(env("FOO=foo\nBAR=$FOO")).to include("BAR" => "bar")
it "expands variables from ENV if not found in .env" do
ENV["FOO"] = "test"
expect(env("BAR=$FOO")).to eql("BAR" => "test")
end

it "reads variables from ENV when expanding if not found in local env" do
it "expands variables from ENV if found in .env during load" do
ENV["FOO"] = "test"
expect(env("BAR=$FOO")).to eql("BAR" => "test")
expect(env("FOO=development\nBAR=${FOO}")["BAR"])
.to eql("test")
end

it "doesn't expand variables from ENV if in local env in overload" do
ENV["FOO"] = "test"
expect(env("FOO=development\nBAR=${FOO}")["BAR"])
.to eql("test")
end

it "expands undefined variables to an empty string" do
Expand Down
25 changes: 23 additions & 2 deletions spec/dotenv_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
let(:env_files) { [] }

it "defaults to .env" do
expect(Dotenv::Environment).to receive(:new).with(expand(".env"))
expect(Dotenv::Environment).to receive(:new).with(expand(".env"),
anything)
.and_return(double(apply: {}, apply!: {}))
subject
end
Expand All @@ -22,7 +23,7 @@
it "expands the path" do
expected = expand("~/.env")
allow(File).to receive(:exist?) { |arg| arg == expected }
expect(Dotenv::Environment).to receive(:new).with(expected)
expect(Dotenv::Environment).to receive(:new).with(expected, anything)
.and_return(double(apply: {}, apply!: {}))
subject
end
Expand Down Expand Up @@ -55,10 +56,17 @@
end

describe "load" do
let(:env_files) { [] }
subject { Dotenv.load(*env_files) }

it_behaves_like "load"

it "initializes the Environment with a truthy is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, true)
.and_return(double(apply: {}, apply!: {}))
subject
end

context "when the file does not exist" do
let(:env_files) { [".env_does_not_exist"] }

Expand All @@ -70,10 +78,17 @@
end

describe "load!" do
let(:env_files) { [] }
subject { Dotenv.load!(*env_files) }

it_behaves_like "load"

it "initializes Environment with truthy is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, true)
.and_return(double(apply: {}, apply!: {}))
subject
end

context "when one file exists and one does not" do
let(:env_files) { [".env", ".env_does_not_exist"] }

Expand All @@ -88,6 +103,12 @@
subject { Dotenv.overload(*env_files) }
it_behaves_like "load"

it "initializes the Environment with a falsey is_load" do
expect(Dotenv::Environment).to receive(:new).with(anything, false)
.and_return(double(apply: {}, apply!: {}))
subject
end

context "when loading a file containing already set variables" do
let(:env_files) { [fixture_path("plain.env")] }

Expand Down