1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
|
require "dotenv/version"
require "dotenv/parser"
require "dotenv/environment"
require "dotenv/missing_keys"
require "dotenv/diff"
# Shim to load environment variables from `.env files into `ENV`.
module Dotenv
extend self
# An internal monitor to synchronize access to ENV in multi-threaded environments.
SEMAPHORE = Monitor.new
private_constant :SEMAPHORE
attr_accessor :instrumenter
# Loads environment variables from one or more `.env` files. See `#parse` for more details.
def load(*filenames, overwrite: false, ignore: true)
parse(*filenames, overwrite: overwrite, ignore: ignore) do |env|
instrument(:load, env: env) do |payload|
update(env, overwrite: overwrite)
end
end
end
# Same as `#load`, but raises Errno::ENOENT if any files don't exist
def load!(*filenames)
load(*filenames, ignore: false)
end
# same as `#load`, but will overwrite existing values in `ENV`
def overwrite(*filenames)
load(*filenames, overwrite: true)
end
alias_method :overload, :overwrite
# same as `#overwrite`, but raises Errno::ENOENT if any files don't exist
def overwrite!(*filenames)
load(*filenames, overwrite: true, ignore: false)
end
alias_method :overload!, :overwrite!
# Parses the given files, yielding for each file if a block is given.
#
# @param filenames [String, Array<String>] Files to parse
# @param overwrite [Boolean] Overwrite existing `ENV` values
# @param ignore [Boolean] Ignore non-existent files
# @param block [Proc] Block to yield for each parsed `Dotenv::Environment`
# @return [Hash] parsed key/value pairs
def parse(*filenames, overwrite: false, ignore: true, &block)
filenames << ".env" if filenames.empty?
filenames = filenames.reverse if overwrite
filenames.reduce({}) do |hash, filename|
begin
env = Environment.new(File.expand_path(filename), overwrite: overwrite)
env = block.call(env) if block
rescue Errno::ENOENT, Errno::EISDIR
raise unless ignore
end
hash.merge! env || {}
end
end
# Save the current `ENV` to be restored later
def save
instrument(:save) do |payload|
@diff = payload[:diff] = Dotenv::Diff.new
end
end
# Restore `ENV` to a given state
#
# @param env [Hash] Hash of keys and values to restore, defaults to the last saved state
# @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error.
def restore(env = @diff&.a, safe: Thread.current == Thread.main)
# No previously saved or provided state to restore
return unless env
diff = Dotenv::Diff.new(b: env)
return unless diff.any?
unless safe
raise ThreadError, <<~EOE.tr("\n", " ")
Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration
of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore
this error.
EOE
end
instrument(:restore, diff: diff) { ENV.replace(env) }
end
# Update `ENV` with the given hash of keys and values
#
# @param env [Hash] Hash of keys and values to set in `ENV`
# @param overwrite [Boolean] Overwrite existing `ENV` values
def update(env = {}, overwrite: false)
instrument(:update) do |payload|
diff = payload[:diff] = Dotenv::Diff.new do
ENV.update(env.transform_keys(&:to_s)) do |key, old_value, new_value|
# This block is called when a key exists. Return the new value if overwrite is true.
overwrite ? new_value : old_value
end
end
diff.env
end
end
# Modify `ENV` for the block and restore it to its previous state afterwards.
#
# Note that the block is synchronized to prevent concurrent modifications to `ENV`,
# so multiple threads will be executed serially.
#
# @param env [Hash] Hash of keys and values to set in `ENV`
def modify(env = {}, &block)
SEMAPHORE.synchronize do
diff = Dotenv::Diff.new
update(env, overwrite: true)
block.call
ensure
restore(diff.a, safe: true)
end
end
def require_keys(*keys)
missing_keys = keys.flatten - ::ENV.keys
return if missing_keys.empty?
raise MissingKeys, missing_keys
end
private
def instrument(name, payload = {}, &block)
if instrumenter
instrumenter.instrument("#{name}.dotenv", payload, &block)
else
block&.call payload
end
end
end
require "dotenv/rails" if defined?(Rails::Railtie)
|