dotenv 2.8.1 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +167 -96
- data/lib/dotenv/autorestore.rb +29 -0
- data/lib/dotenv/cli.rb +13 -11
- data/lib/dotenv/diff.rb +59 -0
- data/lib/dotenv/environment.rb +12 -15
- data/lib/dotenv/load.rb +2 -1
- data/lib/dotenv/log_subscriber.rb +61 -0
- data/lib/dotenv/missing_keys.rb +1 -1
- data/lib/dotenv/parser.rb +65 -65
- data/lib/dotenv/rails-now.rb +10 -0
- data/lib/dotenv/rails.rb +111 -0
- data/lib/dotenv/replay_logger.rb +20 -0
- data/lib/dotenv/substitutions/command.rb +3 -3
- data/lib/dotenv/substitutions/variable.rb +9 -15
- data/lib/dotenv/template.rb +24 -6
- data/lib/dotenv/version.rb +1 -1
- data/lib/dotenv.rb +112 -47
- metadata +13 -8
data/lib/dotenv/parser.rb
CHANGED
|
@@ -2,77 +2,96 @@ require "dotenv/substitutions/variable"
|
|
|
2
2
|
require "dotenv/substitutions/command" if RUBY_VERSION > "1.8.7"
|
|
3
3
|
|
|
4
4
|
module Dotenv
|
|
5
|
+
# Error raised when encountering a syntax error while parsing a .env file.
|
|
5
6
|
class FormatError < SyntaxError; end
|
|
6
7
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# exporting of variables.
|
|
8
|
+
# Parses the `.env` file format into key/value pairs.
|
|
9
|
+
# It allows for variable substitutions, command substitutions, and exporting of variables.
|
|
10
10
|
class Parser
|
|
11
|
-
@substitutions =
|
|
12
|
-
|
|
11
|
+
@substitutions = [
|
|
12
|
+
Dotenv::Substitutions::Command,
|
|
13
|
+
Dotenv::Substitutions::Variable
|
|
14
|
+
]
|
|
13
15
|
|
|
14
16
|
LINE = /
|
|
15
|
-
(?:^|\A)
|
|
16
|
-
\s*
|
|
17
|
-
(
|
|
18
|
-
([\w.]+)
|
|
19
|
-
(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
17
|
+
(?:^|\A) # beginning of line
|
|
18
|
+
\s* # leading whitespace
|
|
19
|
+
(?<export>export\s+)? # optional export
|
|
20
|
+
(?<key>[\w.]+) # key
|
|
21
|
+
(?: # optional separator and value
|
|
22
|
+
(?:\s*=\s*?|:\s+?) # separator
|
|
23
|
+
(?<value> # optional value begin
|
|
24
|
+
\s*'(?:\\'|[^'])*' # single quoted value
|
|
25
|
+
| # or
|
|
26
|
+
\s*"(?:\\"|[^"])*" # double quoted value
|
|
27
|
+
| # or
|
|
28
|
+
[^\#\n]+ # unquoted value
|
|
29
|
+
)? # value end
|
|
30
|
+
)? # separator and value end
|
|
31
|
+
\s* # trailing whitespace
|
|
32
|
+
(?:\#.*)? # optional comment
|
|
33
|
+
(?:$|\z) # end of line
|
|
30
34
|
/x
|
|
31
35
|
|
|
36
|
+
QUOTED_STRING = /\A(['"])(.*)\1\z/m
|
|
37
|
+
|
|
32
38
|
class << self
|
|
33
39
|
attr_reader :substitutions
|
|
34
40
|
|
|
35
|
-
def call(
|
|
36
|
-
new(
|
|
41
|
+
def call(...)
|
|
42
|
+
new(...).call
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
|
|
40
|
-
def initialize(string,
|
|
41
|
-
|
|
46
|
+
def initialize(string, overwrite: false)
|
|
47
|
+
# Convert line breaks to same format
|
|
48
|
+
@string = string.gsub(/\r\n?/, "\n")
|
|
42
49
|
@hash = {}
|
|
43
|
-
@
|
|
50
|
+
@overwrite = overwrite
|
|
44
51
|
end
|
|
45
52
|
|
|
46
53
|
def call
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
@string.scan(LINE) do
|
|
55
|
+
match = $LAST_MATCH_INFO
|
|
56
|
+
|
|
57
|
+
if existing?(match[:key])
|
|
58
|
+
# Use value from already defined variable
|
|
59
|
+
@hash[match[:key]] = ENV[match[:key]]
|
|
60
|
+
elsif match[:export] && !match[:value]
|
|
61
|
+
# Check for exported variable with no value
|
|
62
|
+
if !@hash.member?(match[:key])
|
|
63
|
+
raise FormatError, "Line #{match.to_s.inspect} has an unset variable"
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
@hash[match[:key]] = parse_value(match[:value] || "")
|
|
67
|
+
end
|
|
56
68
|
end
|
|
69
|
+
|
|
57
70
|
@hash
|
|
58
71
|
end
|
|
59
72
|
|
|
60
73
|
private
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
raise FormatError, "Line #{line.inspect} has an unset variable"
|
|
66
|
-
end
|
|
67
|
-
end
|
|
75
|
+
# Determine if a variable is already defined and should not be overwritten.
|
|
76
|
+
def existing?(key)
|
|
77
|
+
!@overwrite && key != "DOTENV_LINEBREAK_MODE" && ENV.key?(key)
|
|
68
78
|
end
|
|
69
79
|
|
|
70
80
|
def parse_value(value)
|
|
71
81
|
# Remove surrounding quotes
|
|
72
|
-
value = value.strip.sub(
|
|
82
|
+
value = value.strip.sub(QUOTED_STRING, '\2')
|
|
73
83
|
maybe_quote = Regexp.last_match(1)
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
|
|
85
|
+
# Expand new lines in double quoted values
|
|
86
|
+
value = expand_newlines(value) if maybe_quote == '"'
|
|
87
|
+
|
|
88
|
+
# Unescape characters and performs substitutions unless value is single quoted
|
|
89
|
+
if maybe_quote != "'"
|
|
90
|
+
value = unescape_characters(value)
|
|
91
|
+
self.class.substitutions.each { |proc| value = proc.call(value, @hash) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
value
|
|
76
95
|
end
|
|
77
96
|
|
|
78
97
|
def unescape_characters(value)
|
|
@@ -80,30 +99,11 @@ module Dotenv
|
|
|
80
99
|
end
|
|
81
100
|
|
|
82
101
|
def expand_newlines(value)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def variable_not_set?(line)
|
|
87
|
-
!line.split[1..-1].all? { |var| @hash.member?(var) }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def unescape_value(value, maybe_quote)
|
|
91
|
-
if maybe_quote == '"'
|
|
92
|
-
unescape_characters(expand_newlines(value))
|
|
93
|
-
elsif maybe_quote.nil?
|
|
94
|
-
unescape_characters(value)
|
|
102
|
+
if (@hash["DOTENV_LINEBREAK_MODE"] || ENV["DOTENV_LINEBREAK_MODE"]) == "legacy"
|
|
103
|
+
value.gsub('\n', "\n").gsub('\r', "\r")
|
|
95
104
|
else
|
|
96
|
-
value
|
|
105
|
+
value.gsub('\n', "\\\\\\n").gsub('\r', "\\\\\\r")
|
|
97
106
|
end
|
|
98
107
|
end
|
|
99
|
-
|
|
100
|
-
def perform_substitutions(value, maybe_quote)
|
|
101
|
-
if maybe_quote != "'"
|
|
102
|
-
self.class.substitutions.each do |proc|
|
|
103
|
-
value = proc.call(value, @hash, @is_load)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
value
|
|
107
|
-
end
|
|
108
108
|
end
|
|
109
109
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# If you use gems that require environment variables to be set before they are
|
|
2
|
+
# loaded, then list `dotenv` in the `Gemfile` before those other gems and
|
|
3
|
+
# require `dotenv/load`.
|
|
4
|
+
#
|
|
5
|
+
# gem "dotenv", require: "dotenv/load"
|
|
6
|
+
# gem "gem-that-requires-env-variables"
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
require "dotenv/load"
|
|
10
|
+
warn '[DEPRECATION] `require "dotenv/rails-now"` is deprecated. Use `require "dotenv/load"` instead.', caller(1..1).first
|
data/lib/dotenv/rails.rb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Since rubygems doesn't support optional dependencies, we have to manually check
|
|
2
|
+
unless Gem::Requirement.new(">= 6.1").satisfied_by?(Gem::Version.new(Rails.version))
|
|
3
|
+
warn "dotenv 3.0 only supports Rails 6.1 or later. Use dotenv ~> 2.0."
|
|
4
|
+
return
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require "dotenv/replay_logger"
|
|
8
|
+
require "dotenv/log_subscriber"
|
|
9
|
+
|
|
10
|
+
Dotenv.instrumenter = ActiveSupport::Notifications
|
|
11
|
+
|
|
12
|
+
# Watch all loaded env files with Spring
|
|
13
|
+
ActiveSupport::Notifications.subscribe("load.dotenv") do |*args|
|
|
14
|
+
if defined?(Spring) && Spring.respond_to?(:watch)
|
|
15
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
16
|
+
Spring.watch event.payload[:env].filename if Rails.application
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Dotenv
|
|
21
|
+
# Rails integration for using Dotenv to load ENV variables from a file
|
|
22
|
+
class Rails < ::Rails::Railtie
|
|
23
|
+
delegate :files, :files=, :overwrite, :overwrite=, :autorestore, :autorestore=, :logger, to: "config.dotenv"
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
super
|
|
27
|
+
config.dotenv = ActiveSupport::OrderedOptions.new.update(
|
|
28
|
+
# Rails.logger is not available yet, so we'll save log messages and replay them when it is
|
|
29
|
+
logger: Dotenv::ReplayLogger.new,
|
|
30
|
+
overwrite: false,
|
|
31
|
+
files: [
|
|
32
|
+
".env.#{env}.local",
|
|
33
|
+
(".env.local" unless env.test?),
|
|
34
|
+
".env.#{env}",
|
|
35
|
+
".env"
|
|
36
|
+
].compact,
|
|
37
|
+
autorestore: env.test? && !defined?(ClimateControl) && !defined?(IceAge)
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Public: Load dotenv
|
|
42
|
+
#
|
|
43
|
+
# This will get called during the `before_configuration` callback, but you
|
|
44
|
+
# can manually call `Dotenv::Rails.load` if you needed it sooner.
|
|
45
|
+
def load
|
|
46
|
+
Dotenv.load(*files.map { |file| root.join(file).to_s }, overwrite: overwrite)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def overload
|
|
50
|
+
deprecator.warn("Dotenv::Rails.overload is deprecated. Set `Dotenv::Rails.overwrite = true` and call Dotenv::Rails.load instead.")
|
|
51
|
+
Dotenv.load(*files.map { |file| root.join(file).to_s }, overwrite: true)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Internal: `Rails.root` is nil in Rails 4.1 before the application is
|
|
55
|
+
# initialized, so this falls back to the `RAILS_ROOT` environment variable,
|
|
56
|
+
# or the current working directory.
|
|
57
|
+
def root
|
|
58
|
+
::Rails.root || Pathname.new(ENV["RAILS_ROOT"] || Dir.pwd)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Set a new logger and replay logs
|
|
62
|
+
def logger=(new_logger)
|
|
63
|
+
logger.replay new_logger if logger.is_a?(ReplayLogger)
|
|
64
|
+
config.dotenv.logger = new_logger
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The current environment that the app is running in.
|
|
68
|
+
#
|
|
69
|
+
# When running `rake`, the Rails application is initialized in development, so we have to
|
|
70
|
+
# check which rake tasks are being run to determine the environment.
|
|
71
|
+
#
|
|
72
|
+
# See https://github.com/bkeepers/dotenv/issues/219
|
|
73
|
+
def env
|
|
74
|
+
@env ||= if defined?(Rake.application) && Rake.application.top_level_tasks.grep(TEST_RAKE_TASKS).any?
|
|
75
|
+
env = Rake.application.options.show_tasks ? "development" : "test"
|
|
76
|
+
ActiveSupport::EnvironmentInquirer.new(env)
|
|
77
|
+
else
|
|
78
|
+
::Rails.env
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
TEST_RAKE_TASKS = /^(default$|test(:|$)|parallel:spec|spec(:|$))/
|
|
82
|
+
|
|
83
|
+
def deprecator # :nodoc:
|
|
84
|
+
@deprecator ||= ActiveSupport::Deprecation.new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Rails uses `#method_missing` to delegate all class methods to the
|
|
88
|
+
# instance, which means `Kernel#load` gets called here. We don't want that.
|
|
89
|
+
def self.load
|
|
90
|
+
instance.load
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
initializer "dotenv", after: :initialize_logger do |app|
|
|
94
|
+
if logger.is_a?(ReplayLogger)
|
|
95
|
+
self.logger = ActiveSupport::TaggedLogging.new(::Rails.logger).tagged("dotenv")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
initializer "dotenv.deprecator" do |app|
|
|
100
|
+
app.deprecators[:dotenv] = deprecator if app.respond_to?(:deprecators)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
initializer "dotenv.autorestore" do |app|
|
|
104
|
+
require "dotenv/autorestore" if autorestore
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
config.before_configuration { load }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
Railtie = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Dotenv::Railtie", "Dotenv::Rails", Dotenv::Rails.deprecator)
|
|
111
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Dotenv
|
|
2
|
+
# A logger that can be used before the apps real logger is initialized.
|
|
3
|
+
class ReplayLogger < Logger
|
|
4
|
+
def initialize
|
|
5
|
+
super(nil) # Doesn't matter what this is, it won't be used.
|
|
6
|
+
@logs = []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Override the add method to store logs so we can replay them to a real logger later.
|
|
10
|
+
def add(*args, &block)
|
|
11
|
+
@logs.push([args, block])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Replay the store logs to a real logger.
|
|
15
|
+
def replay(logger)
|
|
16
|
+
@logs.each { |args, block| logger.add(*args, &block) }
|
|
17
|
+
@logs.clear
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -20,7 +20,7 @@ module Dotenv
|
|
|
20
20
|
)
|
|
21
21
|
/x
|
|
22
22
|
|
|
23
|
-
def call(value,
|
|
23
|
+
def call(value, env)
|
|
24
24
|
# Process interpolated shell commands
|
|
25
25
|
value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
|
|
26
26
|
# Eliminate opening and closing parentheses
|
|
@@ -28,10 +28,10 @@ module Dotenv
|
|
|
28
28
|
|
|
29
29
|
if $LAST_MATCH_INFO[:backslash]
|
|
30
30
|
# Command is escaped, don't replace it.
|
|
31
|
-
$LAST_MATCH_INFO[0][1
|
|
31
|
+
$LAST_MATCH_INFO[0][1..]
|
|
32
32
|
else
|
|
33
33
|
# Execute the command and return the value
|
|
34
|
-
`#{command}`.chomp
|
|
34
|
+
`#{Variable.call(command, env)}`.chomp
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -12,29 +12,23 @@ module Dotenv
|
|
|
12
12
|
VARIABLE = /
|
|
13
13
|
(\\)? # is it escaped with a backslash?
|
|
14
14
|
(\$) # literal $
|
|
15
|
-
(?!\() #
|
|
15
|
+
(?!\() # shouldn't be followed by parenthesis
|
|
16
16
|
\{? # allow brace wrapping
|
|
17
17
|
([A-Z0-9_]+)? # optional alpha nums
|
|
18
18
|
\}? # closing brace
|
|
19
19
|
/xi
|
|
20
20
|
|
|
21
|
-
def call(value, env
|
|
22
|
-
combined_env = is_load ? env.merge(ENV) : ENV.to_h.merge(env)
|
|
21
|
+
def call(value, env)
|
|
23
22
|
value.gsub(VARIABLE) do |variable|
|
|
24
23
|
match = $LAST_MATCH_INFO
|
|
25
|
-
substitute(match, variable, combined_env)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
if match[1] == "\\"
|
|
26
|
+
variable[1..]
|
|
27
|
+
elsif match[3]
|
|
28
|
+
env[match[3]] || ENV[match[3]] || ""
|
|
29
|
+
else
|
|
30
|
+
variable
|
|
31
|
+
end
|
|
38
32
|
end
|
|
39
33
|
end
|
|
40
34
|
end
|
data/lib/dotenv/template.rb
CHANGED
|
@@ -10,17 +10,35 @@ module Dotenv
|
|
|
10
10
|
File.open(@env_file, "r") do |env_file|
|
|
11
11
|
File.open("#{@env_file}.template", "w") do |env_template|
|
|
12
12
|
env_file.each do |line|
|
|
13
|
-
|
|
13
|
+
if is_comment?(line)
|
|
14
|
+
env_template.puts line
|
|
15
|
+
elsif (var = var_defined?(line))
|
|
16
|
+
if line.match(EXPORT_COMMAND)
|
|
17
|
+
env_template.puts "export #{var}=#{var}"
|
|
18
|
+
else
|
|
19
|
+
env_template.puts "#{var}=#{var}"
|
|
20
|
+
end
|
|
21
|
+
elsif line_blank?(line)
|
|
22
|
+
env_template.puts
|
|
23
|
+
end
|
|
14
24
|
end
|
|
15
25
|
end
|
|
16
26
|
end
|
|
17
27
|
end
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def is_comment?(line)
|
|
32
|
+
line.strip.start_with?("#")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def var_defined?(line)
|
|
36
|
+
match = Dotenv::Parser::LINE.match(line)
|
|
37
|
+
match && match[:key]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def line_blank?(line)
|
|
41
|
+
line.strip.length.zero?
|
|
24
42
|
end
|
|
25
43
|
end
|
|
26
44
|
end
|
data/lib/dotenv/version.rb
CHANGED
data/lib/dotenv.rb
CHANGED
|
@@ -1,75 +1,133 @@
|
|
|
1
|
+
require "dotenv/version"
|
|
1
2
|
require "dotenv/parser"
|
|
2
3
|
require "dotenv/environment"
|
|
3
4
|
require "dotenv/missing_keys"
|
|
5
|
+
require "dotenv/diff"
|
|
4
6
|
|
|
5
|
-
#
|
|
7
|
+
# Shim to load environment variables from `.env files into `ENV`.
|
|
6
8
|
module Dotenv
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# An internal monitor to synchronize access to ENV in multi-threaded environments.
|
|
12
|
+
SEMAPHORE = Monitor.new
|
|
13
|
+
private_constant :SEMAPHORE
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
attr_accessor :instrumenter
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
# Loads environment variables from one or more `.env` files. See `#parse` for more details.
|
|
18
|
+
def load(*filenames, overwrite: false, ignore: true)
|
|
19
|
+
parse(*filenames, overwrite: overwrite, ignore: ignore) do |env|
|
|
20
|
+
instrument(:load, env: env) do |payload|
|
|
21
|
+
update(env, overwrite: overwrite)
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
|
|
22
|
-
#
|
|
26
|
+
# Same as `#load`, but raises Errno::ENOENT if any files don't exist
|
|
23
27
|
def load!(*filenames)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
load(*filenames, ignore: false)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# same as `#load`, but will overwrite existing values in `ENV`
|
|
32
|
+
def overwrite(*filenames)
|
|
33
|
+
load(*filenames, overwrite: true)
|
|
34
|
+
end
|
|
35
|
+
alias_method :overload, :overwrite
|
|
36
|
+
|
|
37
|
+
# same as `#overwrite`, but raises Errno::ENOENT if any files don't exist
|
|
38
|
+
def overwrite!(*filenames)
|
|
39
|
+
load(*filenames, overwrite: true, ignore: false)
|
|
28
40
|
end
|
|
41
|
+
alias_method :overload!, :overwrite!
|
|
42
|
+
|
|
43
|
+
# Parses the given files, yielding for each file if a block is given.
|
|
44
|
+
#
|
|
45
|
+
# @param filenames [String, Array<String>] Files to parse
|
|
46
|
+
# @param overwrite [Boolean] Overwrite existing `ENV` values
|
|
47
|
+
# @param ignore [Boolean] Ignore non-existent files
|
|
48
|
+
# @param block [Proc] Block to yield for each parsed `Dotenv::Environment`
|
|
49
|
+
# @return [Hash] parsed key/value pairs
|
|
50
|
+
def parse(*filenames, overwrite: false, ignore: true, &block)
|
|
51
|
+
filenames << ".env" if filenames.empty?
|
|
52
|
+
filenames = filenames.reverse if overwrite
|
|
29
53
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
filenames.reduce({}) do |hash, filename|
|
|
55
|
+
begin
|
|
56
|
+
env = Environment.new(File.expand_path(filename), overwrite: overwrite)
|
|
57
|
+
env = block.call(env) if block
|
|
58
|
+
rescue Errno::ENOENT, Errno::EISDIR
|
|
59
|
+
raise unless ignore
|
|
36
60
|
end
|
|
61
|
+
|
|
62
|
+
hash.merge! env || {}
|
|
37
63
|
end
|
|
38
64
|
end
|
|
39
65
|
|
|
40
|
-
#
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
instrument("dotenv.overload", env: env) { env.apply! }
|
|
66
|
+
# Save the current `ENV` to be restored later
|
|
67
|
+
def save
|
|
68
|
+
instrument(:save) do |payload|
|
|
69
|
+
@diff = payload[:diff] = Dotenv::Diff.new
|
|
45
70
|
end
|
|
46
71
|
end
|
|
47
72
|
|
|
48
|
-
#
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
# Restore `ENV` to a given state
|
|
74
|
+
#
|
|
75
|
+
# @param env [Hash] Hash of keys and values to restore, defaults to the last saved state
|
|
76
|
+
# @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error.
|
|
77
|
+
def restore(env = @diff&.a, safe: Thread.current == Thread.main)
|
|
78
|
+
# No previously saved or provided state to restore
|
|
79
|
+
return unless env
|
|
80
|
+
|
|
81
|
+
diff = Dotenv::Diff.new(b: env)
|
|
82
|
+
return unless diff.any?
|
|
83
|
+
|
|
84
|
+
unless safe
|
|
85
|
+
raise ThreadError, <<~EOE.tr("\n", " ")
|
|
86
|
+
Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration
|
|
87
|
+
of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore
|
|
88
|
+
this error.
|
|
89
|
+
EOE
|
|
54
90
|
end
|
|
91
|
+
instrument(:restore, diff: diff) { ENV.replace(env) }
|
|
55
92
|
end
|
|
56
93
|
|
|
57
|
-
#
|
|
94
|
+
# Update `ENV` with the given hash of keys and values
|
|
58
95
|
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
# @param env [Hash] Hash of keys and values to set in `ENV`
|
|
97
|
+
# @param overwrite [Boolean|:warn] Overwrite existing `ENV` values
|
|
98
|
+
def update(env = {}, overwrite: false)
|
|
99
|
+
instrument(:update) do |payload|
|
|
100
|
+
diff = payload[:diff] = Dotenv::Diff.new do
|
|
101
|
+
ENV.update(env.transform_keys(&:to_s)) do |key, old_value, new_value|
|
|
102
|
+
# This block is called when a key exists. Return the new value if overwrite is true.
|
|
103
|
+
case overwrite
|
|
104
|
+
when :warn
|
|
105
|
+
# not printing the value since that could be a secret
|
|
106
|
+
warn "Warning: dotenv not overwriting ENV[#{key.inspect}]"
|
|
107
|
+
old_value
|
|
108
|
+
when true then new_value
|
|
109
|
+
when false then old_value
|
|
110
|
+
else raise ArgumentError, "Invalid value for overwrite: #{overwrite.inspect}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
diff.env
|
|
65
115
|
end
|
|
66
116
|
end
|
|
67
117
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
118
|
+
# Modify `ENV` for the block and restore it to its previous state afterwards.
|
|
119
|
+
#
|
|
120
|
+
# Note that the block is synchronized to prevent concurrent modifications to `ENV`,
|
|
121
|
+
# so multiple threads will be executed serially.
|
|
122
|
+
#
|
|
123
|
+
# @param env [Hash] Hash of keys and values to set in `ENV`
|
|
124
|
+
def modify(env = {}, &block)
|
|
125
|
+
SEMAPHORE.synchronize do
|
|
126
|
+
diff = Dotenv::Diff.new
|
|
127
|
+
update(env, overwrite: true)
|
|
128
|
+
block.call
|
|
129
|
+
ensure
|
|
130
|
+
restore(diff.a, safe: true)
|
|
73
131
|
end
|
|
74
132
|
end
|
|
75
133
|
|
|
@@ -79,8 +137,15 @@ module Dotenv
|
|
|
79
137
|
raise MissingKeys, missing_keys
|
|
80
138
|
end
|
|
81
139
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def instrument(name, payload = {}, &block)
|
|
143
|
+
if instrumenter
|
|
144
|
+
instrumenter.instrument("#{name}.dotenv", payload, &block)
|
|
145
|
+
else
|
|
146
|
+
block&.call payload
|
|
147
|
+
end
|
|
85
148
|
end
|
|
86
149
|
end
|
|
150
|
+
|
|
151
|
+
require "dotenv/rails" if defined?(Rails::Railtie)
|