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.
@@ -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
@@ -3,7 +3,7 @@ module Dotenv
3
3
 
4
4
  class MissingKeys < Error # :nodoc:
5
5
  def initialize(keys)
6
- key_word = "key#{keys.size > 1 ? 's' : ''}"
6
+ key_word = "key#{"s" if keys.size > 1}"
7
7
  super("Missing required configuration #{key_word}: #{keys.inspect}")
8
8
  end
9
9
  end
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
- # This class enables parsing of a string for key value pairs to be returned
8
- # and stored in the Environment. It allows for variable substitutions and
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
- [Dotenv::Substitutions::Variable, Dotenv::Substitutions::Command]
11
+ @substitutions = [
12
+ Dotenv::Substitutions::Command,
13
+ Dotenv::Substitutions::Variable
14
+ ]
13
15
 
14
16
  LINE = /
15
- (?:^|\A) # beginning of line
16
- \s* # leading whitespace
17
- (?:export\s+)? # optional export
18
- ([\w\.]+) # key
19
- (?:\s*=\s*?|:\s+?) # separator
20
- ( # optional value begin
21
- \s*'(?:\\'|[^'])*' # single quoted value
22
- | # or
23
- \s*"(?:\\"|[^"])*" # double quoted value
24
- | # or
25
- [^\#\r\n]+ # unquoted value
26
- )? # value end
27
- \s* # trailing whitespace
28
- (?:\#.*)? # optional comment
29
- (?:$|\z) # end of line
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(string, is_load = false)
36
- new(string, is_load).call
41
+ def call(...)
42
+ new(...).call
37
43
  end
38
44
  end
39
45
 
40
- def initialize(string, is_load = false)
41
- @string = string
46
+ def initialize(string, overwrite: false)
47
+ # Convert line breaks to same format
48
+ @string = string.gsub(/\r\n?/, "\n")
42
49
  @hash = {}
43
- @is_load = is_load
50
+ @overwrite = overwrite
44
51
  end
45
52
 
46
53
  def call
47
- # Convert line breaks to same format
48
- lines = @string.gsub(/\r\n?/, "\n")
49
- # Process matches
50
- lines.scan(LINE).each do |key, value|
51
- @hash[key] = parse_value(value || "")
52
- end
53
- # Process non-matches
54
- lines.gsub(LINE, "").split(/[\n\r]+/).each do |line|
55
- parse_line(line)
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
- def parse_line(line)
63
- if line.split.first == "export"
64
- if variable_not_set?(line)
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(/\A(['"])(.*)\1\z/m, '\2')
82
+ value = value.strip.sub(QUOTED_STRING, '\2')
83
+ maybe_quote = Regexp.last_match(1)
73
84
 
74
- if Regexp.last_match(1) == '"'
75
- value = unescape_characters(expand_newlines(value))
76
- end
85
+ # Expand new lines in double quoted values
86
+ value = expand_newlines(value) if maybe_quote == '"'
77
87
 
78
- if Regexp.last_match(1) != "'"
79
- self.class.substitutions.each do |proc|
80
- value = proc.call(value, @hash, @is_load)
81
- end
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
- value.gsub('\n', "\n").gsub('\r', "\r")
92
- end
93
-
94
- def variable_not_set?(line)
95
- !line.split[1..-1].all? { |var| @hash.member?(var) }
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
@@ -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, _env, _is_load)
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..-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
- (?!\() # shouldnt be followed by paranthesis
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, is_load)
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
- def substitute(match, variable, env)
36
- if match[1] == '\\'
37
- variable[1..-1]
38
- elsif match[3]
39
- env.fetch(match[3], "")
40
- else
41
- variable
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
@@ -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
- var, value = line.split("=")
13
- is_a_comment = var.strip[0].eql?("#")
14
- line_transform = value.nil? || is_a_comment ? line : "#{var}=#{var}"
15
- env_template.puts line_transform
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
@@ -1,3 +1,3 @@
1
1
  module Dotenv
2
- VERSION = "2.7.6".freeze
2
+ VERSION = "3.2.0".freeze
3
3
  end