File: dotenv.rb

package info (click to toggle)
ruby-dotenv 3.1.8-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 156 kB
  • sloc: ruby: 539; makefile: 4
file content (143 lines) | stat: -rw-r--r-- 4,514 bytes parent folder | download
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)