dotenv 2.7.6 → 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 +171 -92
- data/lib/dotenv/autorestore.rb +29 -0
- data/lib/dotenv/cli.rb +27 -48
- 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 +64 -53
- 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 +4 -4
- data/lib/dotenv/substitutions/variable.rb +9 -19
- data/lib/dotenv/template.rb +27 -4
- data/lib/dotenv/version.rb +1 -1
- data/lib/dotenv.rb +112 -47
- metadata +18 -13
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "active_support/log_subscriber"
|
|
2
|
+
|
|
3
|
+
module Dotenv
|
|
4
|
+
# Logs instrumented events
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# require "active_support/notifications"
|
|
8
|
+
# require "dotenv/log_subscriber"
|
|
9
|
+
# Dotenv.instrumenter = ActiveSupport::Notifications
|
|
10
|
+
#
|
|
11
|
+
class LogSubscriber < ActiveSupport::LogSubscriber
|
|
12
|
+
attach_to :dotenv
|
|
13
|
+
|
|
14
|
+
def logger
|
|
15
|
+
Dotenv::Rails.logger
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def load(event)
|
|
19
|
+
env = event.payload[:env]
|
|
20
|
+
|
|
21
|
+
info "Loaded #{color_filename(env.filename)}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update(event)
|
|
25
|
+
diff = event.payload[:diff]
|
|
26
|
+
changed = diff.env.keys.map { |key| color_var(key) }
|
|
27
|
+
debug "Set #{changed.join(", ")}" if diff.any?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def save(event)
|
|
31
|
+
info "Saved a snapshot of #{color_env_constant}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def restore(event)
|
|
35
|
+
diff = event.payload[:diff]
|
|
36
|
+
|
|
37
|
+
removed = diff.removed.keys.map { |key| color(key, :RED) }
|
|
38
|
+
restored = (diff.changed.keys + diff.added.keys).map { |key| color_var(key) }
|
|
39
|
+
|
|
40
|
+
if removed.any? || restored.any?
|
|
41
|
+
info "Restored snapshot of #{color_env_constant}"
|
|
42
|
+
debug "Unset #{removed.join(", ")}" if removed.any?
|
|
43
|
+
debug "Restored #{restored.join(", ")}" if restored.any?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def color_filename(filename)
|
|
50
|
+
color(Pathname.new(filename).relative_path_from(Dotenv::Rails.root.to_s).to_s, :YELLOW)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def color_var(name)
|
|
54
|
+
color(name, :CYAN)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def color_env_constant
|
|
58
|
+
color("ENV", :GREEN)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/dotenv/missing_keys.rb
CHANGED
data/lib/dotenv/parser.rb
CHANGED
|
@@ -2,84 +2,95 @@ 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')
|
|
83
|
+
maybe_quote = Regexp.last_match(1)
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
85
|
+
# Expand new lines in double quoted values
|
|
86
|
+
value = expand_newlines(value) if maybe_quote == '"'
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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) }
|
|
82
92
|
end
|
|
93
|
+
|
|
83
94
|
value
|
|
84
95
|
end
|
|
85
96
|
|
|
@@ -88,11 +99,11 @@ module Dotenv
|
|
|
88
99
|
end
|
|
89
100
|
|
|
90
101
|
def expand_newlines(value)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
if (@hash["DOTENV_LINEBREAK_MODE"] || ENV["DOTENV_LINEBREAK_MODE"]) == "legacy"
|
|
103
|
+
value.gsub('\n', "\n").gsub('\r', "\r")
|
|
104
|
+
else
|
|
105
|
+
value.gsub('\n', "\\\\\\n").gsub('\r', "\\\\\\r")
|
|
106
|
+
end
|
|
96
107
|
end
|
|
97
108
|
end
|
|
98
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
|
|
@@ -13,14 +13,14 @@ module Dotenv
|
|
|
13
13
|
\$ # literal $
|
|
14
14
|
(?<cmd> # collect command content for eval
|
|
15
15
|
\( # require opening paren
|
|
16
|
-
([^()]|\g<cmd>)+ # allow any number of non-parens, or balanced
|
|
16
|
+
(?:[^()]|\g<cmd>)+ # allow any number of non-parens, or balanced
|
|
17
17
|
# parens (by nesting the <cmd> expression
|
|
18
18
|
# recursively)
|
|
19
19
|
\) # require closing paren
|
|
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,33 +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 = if is_load
|
|
23
|
-
env.merge(ENV)
|
|
24
|
-
else
|
|
25
|
-
ENV.to_h.merge(env)
|
|
26
|
-
end
|
|
21
|
+
def call(value, env)
|
|
27
22
|
value.gsub(VARIABLE) do |variable|
|
|
28
23
|
match = $LAST_MATCH_INFO
|
|
29
|
-
substitute(match, variable, combined_env)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
if match[1] == "\\"
|
|
26
|
+
variable[1..]
|
|
27
|
+
elsif match[3]
|
|
28
|
+
env[match[3]] || ENV[match[3]] || ""
|
|
29
|
+
else
|
|
30
|
+
variable
|
|
31
|
+
end
|
|
42
32
|
end
|
|
43
33
|
end
|
|
44
34
|
end
|
data/lib/dotenv/template.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
module Dotenv
|
|
2
|
+
EXPORT_COMMAND = "export ".freeze
|
|
2
3
|
# Class for creating a template from a env file
|
|
3
4
|
class EnvTemplate
|
|
4
5
|
def initialize(env_file)
|
|
@@ -9,13 +10,35 @@ module Dotenv
|
|
|
9
10
|
File.open(@env_file, "r") do |env_file|
|
|
10
11
|
File.open("#{@env_file}.template", "w") do |env_template|
|
|
11
12
|
env_file.each do |line|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
16
24
|
end
|
|
17
25
|
end
|
|
18
26
|
end
|
|
19
27
|
end
|
|
28
|
+
|
|
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?
|
|
42
|
+
end
|
|
20
43
|
end
|
|
21
44
|
end
|
data/lib/dotenv/version.rb
CHANGED