dotenv.rb 4.3 KB

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