├── .github
├── dependabot.yml
├── funding.yml
├── issue_template.md
├── stale.yml
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .standard.yml
├── Changelog.md
├── Gemfile
├── Guardfile
├── LICENSE
├── OWNERS
├── README.md
├── Rakefile
├── benchmark
├── parse_ips.rb
└── parse_profile.rb
├── bin
└── dotenv
├── dotenv-rails.gemspec
├── dotenv.gemspec
├── lib
├── dotenv-rails.rb
├── dotenv.rb
└── dotenv
│ ├── autorestore.rb
│ ├── cli.rb
│ ├── diff.rb
│ ├── environment.rb
│ ├── load.rb
│ ├── log_subscriber.rb
│ ├── missing_keys.rb
│ ├── parser.rb
│ ├── rails-now.rb
│ ├── rails.rb
│ ├── replay_logger.rb
│ ├── substitutions
│ ├── command.rb
│ └── variable.rb
│ ├── tasks.rb
│ ├── template.rb
│ └── version.rb
├── spec
├── dotenv
│ ├── cli_spec.rb
│ ├── diff_spec.rb
│ ├── environment_spec.rb
│ ├── log_subscriber_spec.rb
│ ├── parser_spec.rb
│ └── rails_spec.rb
├── dotenv_spec.rb
├── fixtures
│ ├── .env
│ ├── .env.development
│ ├── .env.development.local
│ ├── .env.local
│ ├── .env.test
│ ├── bom.env
│ ├── exported.env
│ ├── important.env
│ ├── plain.env
│ ├── quoted.env
│ └── yaml.env
└── spec_helper.rb
└── test
└── autorestore_test.rb
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "bundler"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | - package-ecosystem: "github-actions"
8 | directory: "/"
9 | schedule:
10 | interval: "weekly"
11 |
--------------------------------------------------------------------------------
/.github/funding.yml:
--------------------------------------------------------------------------------
1 | github: [bkeepers]
2 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | ### Steps to reproduce
2 | Tell us how to reproduce the issue.
3 | Show how you included dotenv (Gemfile).
4 | Paste your env using:
5 | ```bash
6 | $ env | grep MYVARIABLETOSHOW
7 | ```
8 | **REMOVE ANY SENSITIVE INFORMATION FROM YOUR OUTPUT**
9 |
10 | ### Expected behavior
11 | Tell us what should happen
12 |
13 | ### Actual behavior
14 | Tell us what happens instead
15 |
16 | ### System configuration
17 | **dotenv version**:
18 |
19 | **Rails version**:
20 |
21 | **Ruby version**:
22 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: wontfix
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *" # Once/day
8 |
9 | jobs:
10 | versions:
11 | name: Get latest versions
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | product: ["ruby", "rails"]
16 | outputs:
17 | ruby: ${{ steps.supported.outputs.ruby }}
18 | rails: ${{ steps.supported.outputs.rails }}
19 | steps:
20 | - id: supported
21 | run: |
22 | product="${{ matrix.product }}"
23 | data=$(curl https://endoflife.date/api/$product.json)
24 | supported=$(echo $data | jq '[.[] | select(.eol > (now | strftime("%Y-%m-%d")))]')
25 | echo "${product}=$(echo $supported | jq -c 'map(.latest)')" >> $GITHUB_OUTPUT
26 | test:
27 | needs: versions
28 | runs-on: ubuntu-latest
29 | name: Test on Ruby ${{ matrix.ruby }} and Rails ${{ matrix.rails }}
30 | strategy:
31 | fail-fast: false
32 | matrix:
33 | ruby: ${{ fromJSON(needs.versions.outputs.ruby) }}
34 | rails: ${{ fromJSON(needs.versions.outputs.rails) }}
35 | env:
36 | RAILS_VERSION: ${{ matrix.rails }}
37 | steps:
38 | - name: Check out repository code
39 | uses: actions/checkout@v4
40 | - name: Set up Ruby
41 | id: setup-ruby
42 | uses: ruby/setup-ruby@v1
43 | with:
44 | bundler-cache: true
45 | ruby-version: ${{ matrix.ruby }}
46 | continue-on-error: true
47 | - name: Incompatible Versions
48 | if: steps.setup-ruby.outcome == 'failure'
49 | run: echo "Ruby ${{ matrix.ruby }} is not supported with Rails ${{ matrix.rails }}"
50 | - name: Run Rake
51 | if: steps.setup-ruby.outcome != 'failure'
52 | run: bundle exec rake
53 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Gem
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | permissions:
9 | id-token: write # mandatory for trusted publishing
10 | contents: write # required for `rake release` to push the release tag
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Set up Ruby
14 | uses: ruby/setup-ruby@v1
15 | with:
16 | bundler-cache: true
17 | ruby-version: ruby
18 | - uses: rubygems/release-gem@v1
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .ruby-version
6 | .yardoc
7 | Gemfile.lock
8 | tmp
9 | vendor
10 | .DS_Store
11 |
--------------------------------------------------------------------------------
/.standard.yml:
--------------------------------------------------------------------------------
1 | ruby_version: 3.0
2 |
3 | ignore:
4 | - lib/dotenv/parser.rb:
5 | - Lint/InheritException
6 |
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | See [Releases](https://github.com/bkeepers/dotenv/releases) for the latest releases and changelogs.
2 |
3 | [View older releases](https://github.com/bkeepers/dotenv/blob/2840d9c4085a398cbde9f164465515b01c26a402/Changelog.md)
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | gemspec name: "dotenv"
3 | gemspec name: "dotenv-rails"
4 |
5 | gem "railties", "~> #{ENV["RAILS_VERSION"] || "7.1"}"
6 | gem "benchmark-ips"
7 | gem "stackprof"
8 |
9 | group :guard do
10 | gem "guard-rspec"
11 | gem "guard-bundler"
12 | gem "rb-fsevent"
13 | end
14 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | guard "bundler" do
2 | watch("Gemfile")
3 | end
4 |
5 | guard "rspec", cmd: "bundle exec rspec" do
6 | watch(%r{^spec/.+_spec\.rb$})
7 | watch(%r{^spec/spec_helper.rb$}) { "spec" }
8 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
9 | end
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Brandon Keepers
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/OWNERS:
--------------------------------------------------------------------------------
1 | # This project is maintained by:
2 | @bkeepers
3 |
4 | # For more information on the OWNERS file, see:
5 | # https://github.com/bkeepers/OWNERS
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # dotenv [](https://badge.fury.io/rb/dotenv)
2 |
3 | Shim to load environment variables from `.env` into `ENV` in *development*.
4 |
5 |
6 |
7 | Stoked Seagull Software
8 |
9 |
10 | Storing [configuration in the environment](http://12factor.net/config) is one of the tenets of a [twelve-factor app](http://12factor.net). Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
11 |
12 | But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. dotenv loads variables from a `.env` file into `ENV` when the environment is bootstrapped.
13 |
14 | ## Installation
15 |
16 | Add this line to the top of your application's Gemfile and run `bundle install`:
17 |
18 | ```ruby
19 | gem 'dotenv', groups: [:development, :test]
20 | ```
21 |
22 | ## Usage
23 |
24 | Add your application configuration to your `.env` file in the root of your project:
25 |
26 | ```shell
27 | S3_BUCKET=YOURS3BUCKET
28 | SECRET_KEY=YOURSECRETKEYGOESHERE
29 | ```
30 |
31 | Whenever your application loads, these variables will be available in `ENV`:
32 |
33 | ```ruby
34 | config.fog_directory = ENV['S3_BUCKET']
35 | ```
36 |
37 | See the [API Docs](https://rubydoc.info/github/bkeepers/dotenv/main) for more.
38 |
39 | ### Rails
40 |
41 | Dotenv will automatically load when your Rails app boots. See [Customizing Rails](#customizing-rails) to change which files are loaded and when.
42 |
43 | ### Sinatra / Ruby
44 |
45 | Load Dotenv as early as possible in your application bootstrap process:
46 |
47 | ```ruby
48 | require 'dotenv/load'
49 |
50 | # or
51 | require 'dotenv'
52 | Dotenv.load
53 | ```
54 |
55 | By default, `load` will look for a file called `.env` in the current working directory. Pass in multiple files and they will be loaded in order. The first value set for a variable will win.
56 |
57 | ```ruby
58 | require 'dotenv'
59 | Dotenv.load('file1.env', 'file2.env')
60 | ```
61 |
62 | ### Autorestore in tests
63 |
64 | Since 3.0, dotenv in a Rails app will automatically restore `ENV` after each test. This means you can modify `ENV` in your tests without fear of leaking state to other tests. It works with both `ActiveSupport::TestCase` and `Rspec`.
65 |
66 | To disable this behavior, set `config.dotenv.autorestore = false` in `config/application.rb` or `config/environments/test.rb`. It is disabled by default if your app uses [climate_control](https://github.com/thoughtbot/climate_control) or [ice_age](https://github.com/dpep/ice_age_rb).
67 |
68 | To use this behavior outside of a Rails app, just `require "dotenv/autorestore"` in your test suite.
69 |
70 | See [`Dotenv.save`](https://rubydoc.info/github/bkeepers/dotenv/main/Dotenv:save), [Dotenv.restore](https://rubydoc.info/github/bkeepers/dotenv/main/Dotenv:restore), and [`Dotenv.modify(hash) { ... }`](https://rubydoc.info/github/bkeepers/dotenv/main/Dotenv:modify) for manual usage.
71 |
72 | ### Rake
73 |
74 | To ensure `.env` is loaded in rake, load the tasks:
75 |
76 | ```ruby
77 | require 'dotenv/tasks'
78 |
79 | task mytask: :dotenv do
80 | # things that require .env
81 | end
82 | ```
83 |
84 | ### CLI
85 |
86 | You can use the `dotenv` executable load `.env` before launching your application:
87 |
88 | ```console
89 | $ dotenv ./script.rb
90 | ```
91 |
92 | The `dotenv` executable also accepts the flag `-f`. Its value should be a comma-separated list of configuration files, in the order of the most important to the least important. All of the files must exist. There _must_ be a space between the flag and its value.
93 |
94 | ```console
95 | $ dotenv -f ".env.local,.env" ./script.rb
96 | ```
97 |
98 | The `dotenv` executable can optionally ignore missing files with the `-i` or `--ignore` flag. For example, if the `.env.local` file does not exist, the following will ignore the missing file and only load the `.env` file.
99 |
100 | ```console
101 | $ dotenv -i -f ".env.local,.env" ./script.rb
102 | ```
103 |
104 | ### Load Order
105 |
106 | If you use gems that require environment variables to be set before they are loaded, then list `dotenv` in the `Gemfile` before those other gems and require `dotenv/load`.
107 |
108 | ```ruby
109 | gem 'dotenv', require: 'dotenv/load'
110 | gem 'gem-that-requires-env-variables'
111 | ```
112 |
113 | ### Customizing Rails
114 |
115 | Dotenv will load the following files depending on `RAILS_ENV`, with the first file having the highest precedence, and `.env` having the lowest precedence:
116 |
117 |
118 |
119 |
120 | Priority |
121 | Environment |
122 | .gitignore it? |
123 | Notes |
124 |
125 |
126 | |
127 | development |
128 | test |
129 | production |
130 | |
131 | |
132 |
133 |
134 |
135 | highest |
136 | .env.development.local |
137 | .env.test.local |
138 | .env.production.local |
139 | Yes |
140 | Environment-specific local overrides |
141 |
142 |
143 | 2nd |
144 | .env.local |
145 | N/A |
146 | .env.local |
147 | Yes |
148 | Local overrides |
149 |
150 |
151 | 3rd |
152 | .env.development |
153 | .env.test |
154 | .env.production |
155 | No |
156 | Shared environment-specific variables |
157 |
158 |
159 | last |
160 | .env |
161 | .env |
162 | .env |
163 | Maybe |
164 | Shared for all environments |
165 |
166 |
167 |
168 |
169 | These files are loaded during the `before_configuration` callback, which is fired when the `Application` constant is defined in `config/application.rb` with `class Application < Rails::Application`. If you need it to be initialized sooner, or need to customize the loading process, you can do so at the top of `application.rb`
170 |
171 | ```ruby
172 | # config/application.rb
173 | Bundler.require(*Rails.groups)
174 |
175 | # Load .env.local in test
176 | Dotenv::Rails.files.unshift(".env.local") if ENV["RAILS_ENV"] == "test"
177 |
178 | module YourApp
179 | class Application < Rails::Application
180 | # ...
181 | end
182 | end
183 | ```
184 |
185 | Available options:
186 |
187 | * `Dotenv::Rails.files` - list of files to be loaded, in order of precedence.
188 | * `Dotenv::Rails.overwrite` - Overwrite existing `ENV` variables with contents of `.env*` files
189 | * `Dotenv::Rails.logger` - The logger to use for dotenv's logging. Defaults to `Rails.logger`
190 | * `Dotenv::Rails.autorestore` - Enable or disable [autorestore](#autorestore-in-tests)
191 |
192 | ### Multi-line values
193 |
194 | Multi-line values with line breaks must be surrounded with double quotes.
195 |
196 | ```shell
197 | PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
198 | ...
199 | HkVN9...
200 | ...
201 | -----END DSA PRIVATE KEY-----"
202 | ```
203 |
204 | Prior to 3.0, dotenv would replace `\n` in quoted strings with a newline, but that behavior is deprecated. To use the old behavior, set `DOTENV_LINEBREAK_MODE=legacy` before any variables that include `\n`:
205 |
206 | ```shell
207 | DOTENV_LINEBREAK_MODE=legacy
208 | PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9...\n-----END DSA PRIVATE KEY-----\n"
209 | ```
210 |
211 | ### Command Substitution
212 |
213 | You need to add the output of a command in one of your variables? Simply add it with `$(your_command)`:
214 |
215 | ```shell
216 | DATABASE_URL="postgres://$(whoami)@localhost/my_database"
217 | ```
218 |
219 | ### Variable Substitution
220 |
221 | You need to add the value of another variable in one of your variables? You can reference the variable with `${VAR}` or often just `$VAR` in unquoted or double-quoted values.
222 |
223 | ```shell
224 | DATABASE_URL="postgres://${USER}@localhost/my_database"
225 | ```
226 |
227 | If a value contains a `$` and it is not intended to be a variable, wrap it in single quotes.
228 |
229 | ```shell
230 | PASSWORD='pas$word'
231 | ```
232 |
233 | ### Comments
234 |
235 | Comments may be added to your file as such:
236 |
237 | ```shell
238 | # This is a comment
239 | SECRET_KEY=YOURSECRETKEYGOESHERE # comment
240 | SECRET_HASH="something-with-a-#-hash"
241 | ```
242 |
243 | ### Exports
244 |
245 | For compatability, you may also add `export` in front of each line so you can `source` the file in bash:
246 |
247 | ```shell
248 | export S3_BUCKET=YOURS3BUCKET
249 | export SECRET_KEY=YOURSECRETKEYGOESHERE
250 | ```
251 |
252 | ### Required Keys
253 |
254 | If a particular configuration value is required but not set, it's appropriate to raise an error.
255 |
256 | To require configuration keys:
257 |
258 | ```ruby
259 | # config/initializers/dotenv.rb
260 |
261 | Dotenv.require_keys("SERVICE_APP_ID", "SERVICE_KEY", "SERVICE_SECRET")
262 | ```
263 |
264 | If any of the configuration keys above are not set, your application will raise an error during initialization. This method is preferred because it prevents runtime errors in a production application due to improper configuration.
265 |
266 | ### Parsing
267 |
268 | To parse a list of env files for programmatic inspection without modifying the ENV:
269 |
270 | ```ruby
271 | Dotenv.parse(".env.local", ".env")
272 | # => {'S3_BUCKET' => 'YOURS3BUCKET', 'SECRET_KEY' => 'YOURSECRETKEYGOESHERE', ...}
273 | ```
274 |
275 | This method returns a hash of the ENV var name/value pairs.
276 |
277 | ### Templates
278 |
279 | You can use the `-t` or `--template` flag on the dotenv cli to create a template of your `.env` file.
280 |
281 | ```console
282 | $ dotenv -t .env
283 | ```
284 | A template will be created in your working directory named `{FILENAME}.template`. So in the above example, it would create a `.env.template` file.
285 |
286 | The template will contain all the environment variables in your `.env` file but with their values set to the variable names.
287 |
288 | ```shell
289 | # .env
290 | S3_BUCKET=YOURS3BUCKET
291 | SECRET_KEY=YOURSECRETKEYGOESHERE
292 | ```
293 |
294 | Would become
295 |
296 | ```shell
297 | # .env.template
298 | S3_BUCKET=S3_BUCKET
299 | SECRET_KEY=SECRET_KEY
300 | ```
301 |
302 | ## Frequently Answered Questions
303 |
304 | ### Can I use dotenv in production?
305 |
306 | dotenv was originally created to load configuration variables into `ENV` in *development*. There are typically better ways to manage configuration in production environments - such as `/etc/environment` managed by [Puppet](https://github.com/puppetlabs/puppet) or [Chef](https://github.com/chef/chef), `heroku config`, etc.
307 |
308 | However, some find dotenv to be a convenient way to configure Rails applications in staging and production environments, and you can do that by defining environment-specific files like `.env.production` or `.env.test`.
309 |
310 | If you use this gem to handle env vars for multiple Rails environments (development, test, production, etc.), please note that env vars that are general to all environments should be stored in `.env`. Then, environment specific env vars should be stored in `.env.`.
311 |
312 | ### Should I commit my .env file?
313 |
314 | Credentials should only be accessible on the machines that need access to them. Never commit sensitive information to a repository that is not needed by every development machine and server.
315 |
316 | Personally, I prefer to commit the `.env` file with development-only settings. This makes it easy for other developers to get started on the project without compromising credentials for other environments. If you follow this advice, make sure that all the credentials for your development environment are different from your other deployments and that the development credentials do not have access to any confidential data.
317 |
318 | ### Why is it not overwriting existing `ENV` variables?
319 |
320 | By default, it **won't** overwrite existing environment variables as dotenv assumes the deployment environment has more knowledge about configuration than the application does. To overwrite existing environment variables you can use `Dotenv.load files, overwrite: true`.
321 |
322 | You can also use the `-o` or `--overwrite` flag on the dotenv cli to overwrite existing `ENV` variables.
323 |
324 | ```console
325 | $ dotenv -o -f ".env.local,.env"
326 | ```
327 |
328 | ## Contributing
329 |
330 | If you want a better idea of how dotenv works, check out the [Ruby Rogues Code Reading of dotenv](https://www.youtube.com/watch?v=lKmY_0uY86s).
331 |
332 | 1. Fork it
333 | 2. Create your feature branch (`git checkout -b my-new-feature`)
334 | 3. Commit your changes (`git commit -am 'Added some feature'`)
335 | 4. Push to the branch (`git push origin my-new-feature`)
336 | 5. Create new Pull Request
337 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 |
3 | require "bundler/gem_helper"
4 | require "rspec/core/rake_task"
5 | require "rake/testtask"
6 | require "standard/rake"
7 |
8 | namespace "dotenv" do
9 | Bundler::GemHelper.install_tasks name: "dotenv"
10 | end
11 |
12 | class DotenvRailsGemHelper < Bundler::GemHelper
13 | def guard_already_tagged
14 | # noop
15 | end
16 |
17 | def tag_version
18 | # noop
19 | end
20 | end
21 |
22 | namespace "dotenv-rails" do
23 | DotenvRailsGemHelper.install_tasks name: "dotenv-rails"
24 | end
25 |
26 | task build: ["dotenv:build", "dotenv-rails:build"]
27 | task install: ["dotenv:install", "dotenv-rails:install"]
28 | task release: ["dotenv:release", "dotenv-rails:release"]
29 |
30 | desc "Run all specs"
31 | RSpec::Core::RakeTask.new(:spec) do |t|
32 | t.rspec_opts = %w[--color]
33 | t.verbose = false
34 | end
35 |
36 | Rake::TestTask.new do |t|
37 | t.test_files = Dir["test/**/*_test.rb"]
38 | end
39 |
40 | task default: [:spec, :test, :standard]
41 |
--------------------------------------------------------------------------------
/benchmark/parse_ips.rb:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | require "dotenv"
3 | require "benchmark/ips"
4 | require "tempfile"
5 |
6 | f = Tempfile.create("benchmark_ips.env")
7 | 1000.times.map { |i| f.puts "VAR_#{i}=#{i}" }
8 | f.close
9 |
10 | Benchmark.ips do |x|
11 | x.report("parse, overwrite:false") { Dotenv.parse(f.path, overwrite: false) }
12 | x.report("parse, overwrite:true") { Dotenv.parse(f.path, overwrite: true) }
13 | end
14 |
15 | File.unlink(f.path)
16 |
--------------------------------------------------------------------------------
/benchmark/parse_profile.rb:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | require "dotenv"
3 | require "stackprof"
4 | require "benchmark/ips"
5 | require "tempfile"
6 |
7 | f = Tempfile.create("benchmark_ips.env")
8 | 1000.times.map { |i| f.puts "VAR_#{i}=#{i}" }
9 | f.close
10 |
11 | profile = StackProf.run(mode: :wall, interval: 1_000) do
12 | 10_000.times do
13 | Dotenv.parse(f.path, overwrite: false)
14 | end
15 | end
16 |
17 | result = StackProf::Report.new(profile)
18 | puts
19 | result.print_text
20 | puts "\n\n\n"
21 | result.print_method(/Dotenv.parse/)
22 |
23 | File.unlink(f.path)
24 |
--------------------------------------------------------------------------------
/bin/dotenv:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "dotenv/cli"
4 | Dotenv::CLI.new(ARGV).run
5 |
--------------------------------------------------------------------------------
/dotenv-rails.gemspec:
--------------------------------------------------------------------------------
1 | require File.expand_path("../lib/dotenv/version", __FILE__)
2 | require "English"
3 |
4 | Gem::Specification.new "dotenv-rails", Dotenv::VERSION do |gem|
5 | gem.authors = ["Brandon Keepers"]
6 | gem.email = ["brandon@opensoul.org"]
7 | gem.description = gem.summary = "Autoload dotenv in Rails."
8 | gem.homepage = "https://github.com/bkeepers/dotenv"
9 | gem.license = "MIT"
10 | gem.files = `git ls-files lib | grep dotenv-rails.rb`.split("\n") + ["README.md", "LICENSE"]
11 |
12 | gem.add_dependency "dotenv", Dotenv::VERSION
13 | gem.add_dependency "railties", ">= 6.1"
14 |
15 | gem.add_development_dependency "spring"
16 |
17 | gem.metadata = {
18 | "changelog_uri" => "https://github.com/bkeepers/dotenv/releases",
19 | "funding_uri" => "https://github.com/sponsors/bkeepers"
20 | }
21 | end
22 |
--------------------------------------------------------------------------------
/dotenv.gemspec:
--------------------------------------------------------------------------------
1 | require File.expand_path("../lib/dotenv/version", __FILE__)
2 | require "English"
3 |
4 | Gem::Specification.new "dotenv", Dotenv::VERSION do |gem|
5 | gem.authors = ["Brandon Keepers"]
6 | gem.email = ["brandon@opensoul.org"]
7 | gem.description = gem.summary = "Loads environment variables from `.env`."
8 | gem.homepage = "https://github.com/bkeepers/dotenv"
9 | gem.license = "MIT"
10 |
11 | gem.files = `git ls-files README.md LICENSE lib bin | grep -v dotenv-rails.rb`.split("\n")
12 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
13 |
14 | gem.add_development_dependency "rake"
15 | gem.add_development_dependency "rspec"
16 | gem.add_development_dependency "standard"
17 |
18 | gem.required_ruby_version = ">= 3.0"
19 |
20 | gem.metadata = {
21 | "changelog_uri" => "https://github.com/bkeepers/dotenv/releases",
22 | "funding_uri" => "https://github.com/sponsors/bkeepers"
23 | }
24 | end
25 |
--------------------------------------------------------------------------------
/lib/dotenv-rails.rb:
--------------------------------------------------------------------------------
1 | require "dotenv"
2 |
--------------------------------------------------------------------------------
/lib/dotenv.rb:
--------------------------------------------------------------------------------
1 | require "dotenv/version"
2 | require "dotenv/parser"
3 | require "dotenv/environment"
4 | require "dotenv/missing_keys"
5 | require "dotenv/diff"
6 |
7 | # Shim to load environment variables from `.env files into `ENV`.
8 | module Dotenv
9 | extend self
10 |
11 | # An internal monitor to synchronize access to ENV in multi-threaded environments.
12 | SEMAPHORE = Monitor.new
13 | private_constant :SEMAPHORE
14 |
15 | attr_accessor :instrumenter
16 |
17 | # Loads environment variables from one or more `.env` files. See `#parse` for more details.
18 | def load(*filenames, overwrite: false, ignore: true)
19 | parse(*filenames, overwrite: overwrite, ignore: ignore) do |env|
20 | instrument(:load, env: env) do |payload|
21 | update(env, overwrite: overwrite)
22 | end
23 | end
24 | end
25 |
26 | # Same as `#load`, but raises Errno::ENOENT if any files don't exist
27 | def load!(*filenames)
28 | load(*filenames, ignore: false)
29 | end
30 |
31 | # same as `#load`, but will overwrite existing values in `ENV`
32 | def overwrite(*filenames)
33 | load(*filenames, overwrite: true)
34 | end
35 | alias_method :overload, :overwrite
36 |
37 | # same as `#overwrite`, but raises Errno::ENOENT if any files don't exist
38 | def overwrite!(*filenames)
39 | load(*filenames, overwrite: true, ignore: false)
40 | end
41 | alias_method :overload!, :overwrite!
42 |
43 | # Parses the given files, yielding for each file if a block is given.
44 | #
45 | # @param filenames [String, Array] Files to parse
46 | # @param overwrite [Boolean] Overwrite existing `ENV` values
47 | # @param ignore [Boolean] Ignore non-existent files
48 | # @param block [Proc] Block to yield for each parsed `Dotenv::Environment`
49 | # @return [Hash] parsed key/value pairs
50 | def parse(*filenames, overwrite: false, ignore: true, &block)
51 | filenames << ".env" if filenames.empty?
52 | filenames = filenames.reverse if overwrite
53 |
54 | filenames.reduce({}) do |hash, filename|
55 | begin
56 | env = Environment.new(File.expand_path(filename), overwrite: overwrite)
57 | env = block.call(env) if block
58 | rescue Errno::ENOENT, Errno::EISDIR
59 | raise unless ignore
60 | end
61 |
62 | hash.merge! env || {}
63 | end
64 | end
65 |
66 | # Save the current `ENV` to be restored later
67 | def save
68 | instrument(:save) do |payload|
69 | @diff = payload[:diff] = Dotenv::Diff.new
70 | end
71 | end
72 |
73 | # Restore `ENV` to a given state
74 | #
75 | # @param env [Hash] Hash of keys and values to restore, defaults to the last saved state
76 | # @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error.
77 | def restore(env = @diff&.a, safe: Thread.current == Thread.main)
78 | # No previously saved or provided state to restore
79 | return unless env
80 |
81 | diff = Dotenv::Diff.new(b: env)
82 | return unless diff.any?
83 |
84 | unless safe
85 | raise ThreadError, <<~EOE.tr("\n", " ")
86 | Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration
87 | of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore
88 | this error.
89 | EOE
90 | end
91 | instrument(:restore, diff: diff) { ENV.replace(env) }
92 | end
93 |
94 | # Update `ENV` with the given hash of keys and values
95 | #
96 | # @param env [Hash] Hash of keys and values to set in `ENV`
97 | # @param overwrite [Boolean] Overwrite existing `ENV` values
98 | def update(env = {}, overwrite: false)
99 | instrument(:update) do |payload|
100 | diff = payload[:diff] = Dotenv::Diff.new do
101 | ENV.update(env.transform_keys(&:to_s)) do |key, old_value, new_value|
102 | # This block is called when a key exists. Return the new value if overwrite is true.
103 | overwrite ? new_value : old_value
104 | end
105 | end
106 | diff.env
107 | end
108 | end
109 |
110 | # Modify `ENV` for the block and restore it to its previous state afterwards.
111 | #
112 | # Note that the block is synchronized to prevent concurrent modifications to `ENV`,
113 | # so multiple threads will be executed serially.
114 | #
115 | # @param env [Hash] Hash of keys and values to set in `ENV`
116 | def modify(env = {}, &block)
117 | SEMAPHORE.synchronize do
118 | diff = Dotenv::Diff.new
119 | update(env, overwrite: true)
120 | block.call
121 | ensure
122 | restore(diff.a, safe: true)
123 | end
124 | end
125 |
126 | def require_keys(*keys)
127 | missing_keys = keys.flatten - ::ENV.keys
128 | return if missing_keys.empty?
129 | raise MissingKeys, missing_keys
130 | end
131 |
132 | private
133 |
134 | def instrument(name, payload = {}, &block)
135 | if instrumenter
136 | instrumenter.instrument("#{name}.dotenv", payload, &block)
137 | else
138 | block&.call payload
139 | end
140 | end
141 | end
142 |
143 | require "dotenv/rails" if defined?(Rails::Railtie)
144 |
--------------------------------------------------------------------------------
/lib/dotenv/autorestore.rb:
--------------------------------------------------------------------------------
1 | # Automatically restore `ENV` to its original state after
2 |
3 | if defined?(RSpec.configure)
4 | RSpec.configure do |config|
5 | # Save ENV before the suite starts
6 | config.before(:suite) { Dotenv.save }
7 |
8 | # Restore ENV after each example
9 | config.after { Dotenv.restore }
10 | end
11 | end
12 |
13 | if defined?(ActiveSupport)
14 | ActiveSupport.on_load(:active_support_test_case) do
15 | ActiveSupport::TestCase.class_eval do
16 | # Save ENV before each test
17 | setup { Dotenv.save }
18 |
19 | # Restore ENV after each test
20 | teardown do
21 | Dotenv.restore
22 | rescue ThreadError => e
23 | # Restore will fail if running tests in parallel.
24 | warn e.message
25 | warn "Set `config.dotenv.autorestore = false` in `config/initializers/test.rb`" if defined?(Dotenv::Rails)
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/dotenv/cli.rb:
--------------------------------------------------------------------------------
1 | require "dotenv"
2 | require "dotenv/version"
3 | require "dotenv/template"
4 | require "optparse"
5 |
6 | module Dotenv
7 | # The `dotenv` command line interface. Run `$ dotenv --help` to see usage.
8 | class CLI < OptionParser
9 | attr_reader :argv, :filenames, :overwrite
10 |
11 | def initialize(argv = [])
12 | @argv = argv.dup
13 | @filenames = []
14 | @ignore = false
15 | @overwrite = false
16 |
17 | super("Usage: dotenv [options]")
18 | separator ""
19 |
20 | on("-f FILES", Array, "List of env files to parse") do |list|
21 | @filenames = list
22 | end
23 |
24 | on("-i", "--ignore", "ignore missing env files") do
25 | @ignore = true
26 | end
27 |
28 | on("-o", "--overwrite", "overwrite existing ENV variables") do
29 | @overwrite = true
30 | end
31 | on("--overload") { @overwrite = true }
32 |
33 | on("-h", "--help", "Display help") do
34 | puts self
35 | exit
36 | end
37 |
38 | on("-v", "--version", "Show version") do
39 | puts "dotenv #{Dotenv::VERSION}"
40 | exit
41 | end
42 |
43 | on("-t", "--template=FILE", "Create a template env file") do |file|
44 | template = Dotenv::EnvTemplate.new(file)
45 | template.create_template
46 | end
47 |
48 | order!(@argv)
49 | end
50 |
51 | def run
52 | Dotenv.load(*@filenames, overwrite: @overwrite, ignore: @ignore)
53 | rescue Errno::ENOENT => e
54 | abort e.message
55 | else
56 | exec(*@argv) unless @argv.empty?
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/dotenv/diff.rb:
--------------------------------------------------------------------------------
1 | module Dotenv
2 | # A diff between multiple states of ENV.
3 | class Diff
4 | # The initial state
5 | attr_reader :a
6 |
7 | # The final or current state
8 | attr_reader :b
9 |
10 | # Create a new diff. If given a block, the state of ENV after the block will be preserved as
11 | # the final state for comparison. Otherwise, the current ENV will be the final state.
12 | #
13 | # @param a [Hash] the initial state, defaults to a snapshot of current ENV
14 | # @param b [Hash] the final state, defaults to the current ENV
15 | # @yield [diff] a block to execute before recording the final state
16 | def initialize(a: snapshot, b: ENV, &block)
17 | @a, @b = a, b
18 | block&.call self
19 | ensure
20 | @b = snapshot if block
21 | end
22 |
23 | # Return a Hash of keys added with their new values
24 | def added
25 | b.slice(*(b.keys - a.keys))
26 | end
27 |
28 | # Returns a Hash of keys removed with their previous values
29 | def removed
30 | a.slice(*(a.keys - b.keys))
31 | end
32 |
33 | # Returns of Hash of keys changed with an array of their previous and new values
34 | def changed
35 | (b.slice(*a.keys).to_a - a.to_a).map do |(k, v)|
36 | [k, [a[k], v]]
37 | end.to_h
38 | end
39 |
40 | # Returns a Hash of all added, changed, and removed keys and their new values
41 | def env
42 | b.slice(*(added.keys + changed.keys)).merge(removed.transform_values { |v| nil })
43 | end
44 |
45 | # Returns true if any keys were added, removed, or changed
46 | def any?
47 | [added, removed, changed].any?(&:any?)
48 | end
49 |
50 | private
51 |
52 | def snapshot
53 | # `dup` should not be required here, but some people use `stub_const` to replace ENV with
54 | # a `Hash`. This ensures that we get a frozen copy of that instead of freezing the original.
55 | # https://github.com/bkeepers/dotenv/issues/482
56 | ENV.to_h.dup.freeze
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/dotenv/environment.rb:
--------------------------------------------------------------------------------
1 | module Dotenv
2 | # A `.env` file that will be read and parsed into a Hash
3 | class Environment < Hash
4 | attr_reader :filename, :overwrite
5 |
6 | # Create a new Environment
7 | #
8 | # @param filename [String] the path to the file to read
9 | # @param overwrite [Boolean] whether the parser should assume existing values will be overwritten
10 | def initialize(filename, overwrite: false)
11 | super()
12 | @filename = filename
13 | @overwrite = overwrite
14 | load
15 | end
16 |
17 | def load
18 | update Parser.call(read, overwrite: overwrite)
19 | end
20 |
21 | def read
22 | File.open(@filename, "rb:bom|utf-8", &:read)
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/dotenv/load.rb:
--------------------------------------------------------------------------------
1 | require "dotenv"
2 |
3 | defined?(Dotenv::Rails) ? Dotenv::Rails.load : Dotenv.load
4 |
--------------------------------------------------------------------------------
/lib/dotenv/log_subscriber.rb:
--------------------------------------------------------------------------------
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.to_sentence}" 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.to_sentence}" if removed.any?
43 | debug "Restored #{restored.to_sentence}" 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
62 |
--------------------------------------------------------------------------------
/lib/dotenv/missing_keys.rb:
--------------------------------------------------------------------------------
1 | module Dotenv
2 | class Error < StandardError; end
3 |
4 | class MissingKeys < Error # :nodoc:
5 | def initialize(keys)
6 | key_word = "key#{(keys.size > 1) ? "s" : ""}"
7 | super("Missing required configuration #{key_word}: #{keys.inspect}")
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/dotenv/parser.rb:
--------------------------------------------------------------------------------
1 | require "dotenv/substitutions/variable"
2 | require "dotenv/substitutions/command" if RUBY_VERSION > "1.8.7"
3 |
4 | module Dotenv
5 | # Error raised when encountering a syntax error while parsing a .env file.
6 | class FormatError < SyntaxError; end
7 |
8 | # Parses the `.env` file format into key/value pairs.
9 | # It allows for variable substitutions, command substitutions, and exporting of variables.
10 | class Parser
11 | @substitutions = [
12 | Dotenv::Substitutions::Variable,
13 | Dotenv::Substitutions::Command
14 | ]
15 |
16 | LINE = /
17 | (?:^|\A) # beginning of line
18 | \s* # leading whitespace
19 | (?export\s+)? # optional export
20 | (?[\w.]+) # key
21 | (?: # optional separator and value
22 | (?:\s*=\s*?|:\s+?) # separator
23 | (? # 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
34 | /x
35 |
36 | QUOTED_STRING = /\A(['"])(.*)\1\z/m
37 |
38 | class << self
39 | attr_reader :substitutions
40 |
41 | def call(...)
42 | new(...).call
43 | end
44 | end
45 |
46 | def initialize(string, overwrite: false)
47 | # Convert line breaks to same format
48 | @string = string.gsub(/\r\n?/, "\n")
49 | @hash = {}
50 | @overwrite = overwrite
51 | end
52 |
53 | def call
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
68 | end
69 |
70 | @hash
71 | end
72 |
73 | private
74 |
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)
78 | end
79 |
80 | def parse_value(value)
81 | # Remove surrounding quotes
82 | value = value.strip.sub(QUOTED_STRING, '\2')
83 | maybe_quote = Regexp.last_match(1)
84 |
85 | # Expand new lines in double quoted values
86 | value = expand_newlines(value) if maybe_quote == '"'
87 |
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) }
92 | end
93 |
94 | value
95 | end
96 |
97 | def unescape_characters(value)
98 | value.gsub(/\\([^$])/, '\1')
99 | end
100 |
101 | def expand_newlines(value)
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
107 | end
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/lib/dotenv/rails-now.rb:
--------------------------------------------------------------------------------
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
11 |
--------------------------------------------------------------------------------
/lib/dotenv/rails.rb:
--------------------------------------------------------------------------------
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
112 |
--------------------------------------------------------------------------------
/lib/dotenv/replay_logger.rb:
--------------------------------------------------------------------------------
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
21 |
--------------------------------------------------------------------------------
/lib/dotenv/substitutions/command.rb:
--------------------------------------------------------------------------------
1 | require "English"
2 |
3 | module Dotenv
4 | module Substitutions
5 | # Substitute shell commands in a value.
6 | #
7 | # SHA=$(git rev-parse HEAD)
8 | #
9 | module Command
10 | class << self
11 | INTERPOLATED_SHELL_COMMAND = /
12 | (?\\)? # is it escaped with a backslash?
13 | \$ # literal $
14 | (? # collect command content for eval
15 | \( # require opening paren
16 | (?:[^()]|\g)+ # allow any number of non-parens, or balanced
17 | # parens (by nesting the expression
18 | # recursively)
19 | \) # require closing paren
20 | )
21 | /x
22 |
23 | def call(value, _env)
24 | # Process interpolated shell commands
25 | value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
26 | # Eliminate opening and closing parentheses
27 | command = $LAST_MATCH_INFO[:cmd][1..-2]
28 |
29 | if $LAST_MATCH_INFO[:backslash]
30 | # Command is escaped, don't replace it.
31 | $LAST_MATCH_INFO[0][1..]
32 | else
33 | # Execute the command and return the value
34 | `#{command}`.chomp
35 | end
36 | end
37 | end
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/dotenv/substitutions/variable.rb:
--------------------------------------------------------------------------------
1 | require "English"
2 |
3 | module Dotenv
4 | module Substitutions
5 | # Substitute variables in a value.
6 | #
7 | # HOST=example.com
8 | # URL="https://$HOST"
9 | #
10 | module Variable
11 | class << self
12 | VARIABLE = /
13 | (\\)? # is it escaped with a backslash?
14 | (\$) # literal $
15 | (?!\() # shouldn't be followed by parenthesis
16 | \{? # allow brace wrapping
17 | ([A-Z0-9_]+)? # optional alpha nums
18 | \}? # closing brace
19 | /xi
20 |
21 | def call(value, env)
22 | value.gsub(VARIABLE) do |variable|
23 | match = $LAST_MATCH_INFO
24 |
25 | if match[1] == "\\"
26 | variable[1..]
27 | elsif match[3]
28 | env[match[3]] || ENV[match[3]] || ""
29 | else
30 | variable
31 | end
32 | end
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/dotenv/tasks.rb:
--------------------------------------------------------------------------------
1 | desc "Load environment settings from .env"
2 | task :dotenv do
3 | require "dotenv"
4 | Dotenv.load
5 | end
6 |
7 | task environment: :dotenv
8 |
--------------------------------------------------------------------------------
/lib/dotenv/template.rb:
--------------------------------------------------------------------------------
1 | module Dotenv
2 | EXPORT_COMMAND = "export ".freeze
3 | # Class for creating a template from a env file
4 | class EnvTemplate
5 | def initialize(env_file)
6 | @env_file = env_file
7 | end
8 |
9 | def create_template
10 | File.open(@env_file, "r") do |env_file|
11 | File.open("#{@env_file}.template", "w") do |env_template|
12 | env_file.each do |line|
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
24 | end
25 | end
26 | end
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
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/dotenv/version.rb:
--------------------------------------------------------------------------------
1 | module Dotenv
2 | VERSION = "3.1.8".freeze
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dotenv/cli_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "dotenv/cli"
3 |
4 | describe "dotenv binary" do
5 | before do
6 | Dir.chdir(File.expand_path("../../fixtures", __FILE__))
7 | end
8 |
9 | def run(*args)
10 | Dotenv::CLI.new(args).run
11 | end
12 |
13 | it "loads from .env by default" do
14 | expect(ENV).not_to have_key("DOTENV")
15 | run
16 | expect(ENV).to have_key("DOTENV")
17 | end
18 |
19 | it "loads from file specified by -f" do
20 | expect(ENV).not_to have_key("OPTION_A")
21 | run "-f", "plain.env"
22 | expect(ENV).to have_key("OPTION_A")
23 | end
24 |
25 | it "dies if file specified by -f doesn't exist" do
26 | expect do
27 | capture_output { run "-f", ".doesnotexist" }
28 | end.to raise_error(SystemExit, /No such file/)
29 | end
30 |
31 | it "ignores missing files when --ignore flag given" do
32 | expect do
33 | run "--ignore", "-f", ".doesnotexist"
34 | end.not_to raise_error
35 | end
36 |
37 | it "loads from multiple files specified by -f" do
38 | expect(ENV).not_to have_key("PLAIN")
39 | expect(ENV).not_to have_key("QUOTED")
40 |
41 | run "-f", "plain.env,quoted.env"
42 |
43 | expect(ENV).to have_key("PLAIN")
44 | expect(ENV).to have_key("QUOTED")
45 | end
46 |
47 | it "does not consume non-dotenv flags by accident" do
48 | cli = Dotenv::CLI.new(["-f", "plain.env", "foo", "--switch"])
49 |
50 | expect(cli.filenames).to eql(["plain.env"])
51 | expect(cli.argv).to eql(["foo", "--switch"])
52 | end
53 |
54 | it "does not consume dotenv flags from subcommand" do
55 | cli = Dotenv::CLI.new(["foo", "-f", "something"])
56 |
57 | expect(cli.filenames).to eql([])
58 | expect(cli.argv).to eql(["foo", "-f", "something"])
59 | end
60 |
61 | it "does not mess with quoted args" do
62 | cli = Dotenv::CLI.new(["foo something"])
63 |
64 | expect(cli.filenames).to eql([])
65 | expect(cli.argv).to eql(["foo something"])
66 | end
67 |
68 | describe "templates a file specified by -t" do
69 | before do
70 | @buffer = StringIO.new
71 | @origin_filename = "plain.env"
72 | @template_filename = "plain.env.template"
73 | end
74 | it "templates variables" do
75 | @input = StringIO.new("FOO=BAR\nFOO2=BAR2")
76 | allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
77 | allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
78 | # call the function that writes to the file
79 | Dotenv::CLI.new(["-t", @origin_filename])
80 | # reading the buffer and checking its content.
81 | expect(@buffer.string).to eq("FOO=FOO\nFOO2=FOO2\n")
82 | end
83 |
84 | it "templates variables with export prefix" do
85 | @input = StringIO.new("export FOO=BAR\nexport FOO2=BAR2")
86 | allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
87 | allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
88 | Dotenv::CLI.new(["-t", @origin_filename])
89 | expect(@buffer.string).to eq("export FOO=FOO\nexport FOO2=FOO2\n")
90 | end
91 |
92 | it "templates multi-line variables" do
93 | @input = StringIO.new(<<~TEXT)
94 | FOO=BAR
95 | FOO2="BAR2
96 | BAR2"
97 | TEXT
98 | allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
99 | allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
100 | # call the function that writes to the file
101 | Dotenv::CLI.new(["-t", @origin_filename])
102 | # reading the buffer and checking its content.
103 | expect(@buffer.string).to eq("FOO=FOO\nFOO2=FOO2\n")
104 | end
105 |
106 | it "ignores blank lines" do
107 | @input = StringIO.new("\nFOO=BAR\nFOO2=BAR2")
108 | allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
109 | allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
110 | Dotenv::CLI.new(["-t", @origin_filename])
111 | expect(@buffer.string).to eq("\nFOO=FOO\nFOO2=FOO2\n")
112 | end
113 |
114 | it "ignores comments" do
115 | @comment_input = StringIO.new("#Heading comment\nFOO=BAR\nFOO2=BAR2\n")
116 | allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_input)
117 | allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
118 | Dotenv::CLI.new(["-t", @origin_filename])
119 | expect(@buffer.string).to eq("#Heading comment\nFOO=FOO\nFOO2=FOO2\n")
120 | end
121 |
122 | it "ignores comments with =" do
123 | @comment_with_equal_input = StringIO.new("#Heading=comment\nFOO=BAR\nFOO2=BAR2")
124 | allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_with_equal_input)
125 | allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
126 | Dotenv::CLI.new(["-t", @origin_filename])
127 | expect(@buffer.string).to eq("#Heading=comment\nFOO=FOO\nFOO2=FOO2\n")
128 | end
129 |
130 | it "ignores comments with leading spaces" do
131 | @comment_leading_spaces_input = StringIO.new(" #Heading comment\nFOO=BAR\nFOO2=BAR2")
132 | allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_leading_spaces_input)
133 | allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
134 | Dotenv::CLI.new(["-t", @origin_filename])
135 | expect(@buffer.string).to eq(" #Heading comment\nFOO=FOO\nFOO2=FOO2\n")
136 | end
137 | end
138 |
139 | # Capture output to $stdout and $stderr
140 | def capture_output(&_block)
141 | original_stderr = $stderr
142 | original_stdout = $stdout
143 | output = $stderr = $stdout = StringIO.new
144 |
145 | yield
146 |
147 | $stderr = original_stderr
148 | $stdout = original_stdout
149 | output.string
150 | end
151 | end
152 |
--------------------------------------------------------------------------------
/spec/dotenv/diff_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Dotenv::Diff do
4 | let(:before) { {} }
5 | let(:after) { {} }
6 | subject { Dotenv::Diff.new(a: before, b: after) }
7 |
8 | context "no changes" do
9 | let(:before) { {"A" => 1} }
10 | let(:after) { {"A" => 1} }
11 |
12 | it { expect(subject.added).to eq({}) }
13 | it { expect(subject.removed).to eq({}) }
14 | it { expect(subject.changed).to eq({}) }
15 | it { expect(subject.any?).to eq(false) }
16 | it { expect(subject.env).to eq({}) }
17 | end
18 |
19 | context "key added" do
20 | let(:after) { {"A" => 1} }
21 |
22 | it { expect(subject.added).to eq("A" => 1) }
23 | it { expect(subject.removed).to eq({}) }
24 | it { expect(subject.changed).to eq({}) }
25 | it { expect(subject.any?).to eq(true) }
26 | it { expect(subject.env).to eq("A" => 1) }
27 | end
28 |
29 | context "key removed" do
30 | let(:before) { {"A" => 1} }
31 |
32 | it { expect(subject.added).to eq({}) }
33 | it { expect(subject.removed).to eq("A" => 1) }
34 | it { expect(subject.changed).to eq({}) }
35 | it { expect(subject.any?).to eq(true) }
36 | it { expect(subject.env).to eq("A" => nil) }
37 | end
38 |
39 | context "key changed" do
40 | let(:before) { {"A" => 1} }
41 | let(:after) { {"A" => 2} }
42 |
43 | it { expect(subject.added).to eq({}) }
44 | it { expect(subject.removed).to eq({}) }
45 | it { expect(subject.changed).to eq("A" => [1, 2]) }
46 | it { expect(subject.any?).to eq(true) }
47 | it { expect(subject.env).to eq("A" => 2) }
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/spec/dotenv/environment_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Dotenv::Environment do
4 | subject { env("OPTION_A=1\nOPTION_B=2") }
5 |
6 | describe "initialize" do
7 | it "reads the file" do
8 | expect(subject["OPTION_A"]).to eq("1")
9 | expect(subject["OPTION_B"]).to eq("2")
10 | end
11 |
12 | it "fails if file does not exist" do
13 | expect do
14 | Dotenv::Environment.new(".does_not_exists")
15 | end.to raise_error(Errno::ENOENT)
16 | end
17 | end
18 |
19 | require "tempfile"
20 | def env(text, ...)
21 | file = Tempfile.new("dotenv")
22 | file.write text
23 | file.close
24 | env = Dotenv::Environment.new(file.path, ...)
25 | file.unlink
26 | env
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/spec/dotenv/log_subscriber_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "active_support/all"
3 | require "rails"
4 | require "dotenv/rails"
5 |
6 | describe Dotenv::LogSubscriber do
7 | let(:logs) { StringIO.new }
8 |
9 | before do
10 | Dotenv.instrumenter = ActiveSupport::Notifications
11 | Dotenv::Rails.logger = Logger.new(logs)
12 | end
13 |
14 | describe "load" do
15 | it "logs when a file is loaded" do
16 | Dotenv.load(fixture_path("plain.env"))
17 | expect(logs.string).to match(/Loaded.*plain.env/)
18 | expect(logs.string).to match(/Set.*PLAIN/)
19 | end
20 | end
21 |
22 | context "update" do
23 | it "logs when a new instance variable is set" do
24 | Dotenv.update({"PLAIN" => "true"})
25 | expect(logs.string).to match(/Set.*PLAIN/)
26 | end
27 |
28 | it "logs when an instance variable is overwritten" do
29 | ENV["PLAIN"] = "nope"
30 | Dotenv.update({"PLAIN" => "true"}, overwrite: true)
31 | expect(logs.string).to match(/Set.*PLAIN/)
32 | end
33 |
34 | it "does not log when an instance variable is not overwritten" do
35 | ENV["FOO"] = "existing"
36 | Dotenv.update({"FOO" => "new"})
37 | expect(logs.string).not_to match(/FOO/)
38 | end
39 |
40 | it "does not log when an instance variable is unchanged" do
41 | ENV["PLAIN"] = "true"
42 | Dotenv.update({"PLAIN" => "true"}, overwrite: true)
43 | expect(logs.string).not_to match(/PLAIN/)
44 | end
45 | end
46 |
47 | context "save" do
48 | it "logs when a snapshot is saved" do
49 | Dotenv.save
50 | expect(logs.string).to match(/Saved/)
51 | end
52 | end
53 |
54 | context "restore" do
55 | it "logs restored keys" do
56 | previous_value = ENV["PWD"]
57 | ENV["PWD"] = "/tmp"
58 | Dotenv.restore
59 |
60 | expect(logs.string).to match(/Restored.*PWD/)
61 |
62 | # Does not log value
63 | expect(logs.string).not_to include(previous_value)
64 | end
65 |
66 | it "logs unset keys" do
67 | ENV["DOTENV_TEST"] = "LogSubscriber"
68 | Dotenv.restore
69 | expect(logs.string).to match(/Unset.*DOTENV_TEST/)
70 | end
71 |
72 | it "does not log if no keys unset or restored" do
73 | Dotenv.restore
74 | expect(logs.string).not_to match(/Restored|Unset/)
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/spec/dotenv/parser_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Dotenv::Parser do
4 | def env(...)
5 | Dotenv::Parser.call(...)
6 | end
7 |
8 | it "parses unquoted values" do
9 | expect(env("FOO=bar")).to eql("FOO" => "bar")
10 | end
11 |
12 | it "parses unquoted values with spaces after seperator" do
13 | expect(env("FOO= bar")).to eql("FOO" => "bar")
14 | end
15 |
16 | it "parses unquoted escape characters correctly" do
17 | expect(env("FOO=bar\\ bar")).to eql("FOO" => "bar bar")
18 | end
19 |
20 | it "parses values with spaces around equal sign" do
21 | expect(env("FOO =bar")).to eql("FOO" => "bar")
22 | expect(env("FOO= bar")).to eql("FOO" => "bar")
23 | end
24 |
25 | it "parses values with leading spaces" do
26 | expect(env(" FOO=bar")).to eql("FOO" => "bar")
27 | end
28 |
29 | it "parses values with following spaces" do
30 | expect(env("FOO=bar ")).to eql("FOO" => "bar")
31 | end
32 |
33 | it "parses double quoted values" do
34 | expect(env('FOO="bar"')).to eql("FOO" => "bar")
35 | end
36 |
37 | it "parses double quoted values with following spaces" do
38 | expect(env('FOO="bar" ')).to eql("FOO" => "bar")
39 | end
40 |
41 | it "parses single quoted values" do
42 | expect(env("FOO='bar'")).to eql("FOO" => "bar")
43 | end
44 |
45 | it "parses single quoted values with following spaces" do
46 | expect(env("FOO='bar' ")).to eql("FOO" => "bar")
47 | end
48 |
49 | it "parses escaped double quotes" do
50 | expect(env('FOO="escaped\"bar"')).to eql("FOO" => 'escaped"bar')
51 | end
52 |
53 | it "parses empty values" do
54 | expect(env("FOO=")).to eql("FOO" => "")
55 | end
56 |
57 | it "expands variables found in values" do
58 | expect(env("FOO=test\nBAR=$FOO")).to eql("FOO" => "test", "BAR" => "test")
59 | end
60 |
61 | it "parses variables wrapped in brackets" do
62 | expect(env("FOO=test\nBAR=${FOO}bar"))
63 | .to eql("FOO" => "test", "BAR" => "testbar")
64 | end
65 |
66 | it "expands variables from ENV if not found in .env" do
67 | ENV["FOO"] = "test"
68 | expect(env("BAR=$FOO")).to eql("BAR" => "test")
69 | end
70 |
71 | it "expands variables from ENV if found in .env during load" do
72 | ENV["FOO"] = "test"
73 | expect(env("FOO=development\nBAR=${FOO}")["BAR"])
74 | .to eql("test")
75 | end
76 |
77 | it "doesn't expand variables from ENV if in local env in overwrite" do
78 | ENV["FOO"] = "test"
79 | expect(env("FOO=development\nBAR=${FOO}")["BAR"])
80 | .to eql("test")
81 | end
82 |
83 | it "expands undefined variables to an empty string" do
84 | expect(env("BAR=$FOO")).to eql("BAR" => "")
85 | end
86 |
87 | it "expands variables in double quoted strings" do
88 | expect(env("FOO=test\nBAR=\"quote $FOO\""))
89 | .to eql("FOO" => "test", "BAR" => "quote test")
90 | end
91 |
92 | it "does not expand variables in single quoted strings" do
93 | expect(env("BAR='quote $FOO'")).to eql("BAR" => "quote $FOO")
94 | end
95 |
96 | it "does not expand escaped variables" do
97 | expect(env('FOO="foo\$BAR"')).to eql("FOO" => "foo$BAR")
98 | expect(env('FOO="foo\${BAR}"')).to eql("FOO" => "foo${BAR}")
99 | expect(env("FOO=test\nBAR=\"foo\\${FOO} ${FOO}\""))
100 | .to eql("FOO" => "test", "BAR" => "foo${FOO} test")
101 | end
102 |
103 | it "parses yaml style options" do
104 | expect(env("OPTION_A: 1")).to eql("OPTION_A" => "1")
105 | end
106 |
107 | it "parses export keyword" do
108 | expect(env("export OPTION_A=2")).to eql("OPTION_A" => "2")
109 | end
110 |
111 | it "allows export line if you want to do it that way" do
112 | expect(env('OPTION_A=2
113 | export OPTION_A')).to eql("OPTION_A" => "2")
114 | end
115 |
116 | it "allows export line if you want to do it that way and checks for unset variables" do
117 | expect do
118 | env('OPTION_A=2
119 | export OH_NO_NOT_SET')
120 | end.to raise_error(Dotenv::FormatError, 'Line "export OH_NO_NOT_SET" has an unset variable')
121 | end
122 |
123 | it 'escapes \n in quoted strings' do
124 | expect(env('FOO="bar\nbaz"')).to eql("FOO" => "bar\\nbaz")
125 | expect(env('FOO="bar\\nbaz"')).to eql("FOO" => "bar\\nbaz")
126 | end
127 |
128 | it 'expands \n and \r in quoted strings with DOTENV_LINEBREAK_MODE=legacy in current file' do
129 | ENV["DOTENV_LINEBREAK_MODE"] = "strict"
130 |
131 | contents = [
132 | "DOTENV_LINEBREAK_MODE=legacy",
133 | 'FOO="bar\nbaz\rfizz"'
134 | ].join("\n")
135 | expect(env(contents)).to eql("DOTENV_LINEBREAK_MODE" => "legacy", "FOO" => "bar\nbaz\rfizz")
136 | end
137 |
138 | it 'expands \n and \r in quoted strings with DOTENV_LINEBREAK_MODE=legacy in ENV' do
139 | ENV["DOTENV_LINEBREAK_MODE"] = "legacy"
140 | contents = 'FOO="bar\nbaz\rfizz"'
141 | expect(env(contents)).to eql("FOO" => "bar\nbaz\rfizz")
142 | end
143 |
144 | it 'parses variables with "." in the name' do
145 | expect(env("FOO.BAR=foobar")).to eql("FOO.BAR" => "foobar")
146 | end
147 |
148 | it "strips unquoted values" do
149 | expect(env("foo=bar ")).to eql("foo" => "bar") # not 'bar '
150 | end
151 |
152 | it "ignores lines that are not variable assignments" do
153 | expect(env("lol$wut")).to eql({})
154 | end
155 |
156 | it "ignores empty lines" do
157 | expect(env("\n \t \nfoo=bar\n \nfizz=buzz"))
158 | .to eql("foo" => "bar", "fizz" => "buzz")
159 | end
160 |
161 | it "does not ignore empty lines in quoted string" do
162 | value = "a\n\nb\n\nc"
163 | expect(env("FOO=\"#{value}\"")).to eql("FOO" => value)
164 | end
165 |
166 | it "ignores inline comments" do
167 | expect(env("foo=bar # this is foo")).to eql("foo" => "bar")
168 | end
169 |
170 | it "allows # in quoted value" do
171 | expect(env('foo="bar#baz" # comment')).to eql("foo" => "bar#baz")
172 | end
173 |
174 | it "allows # in quoted value with spaces after seperator" do
175 | expect(env('foo= "bar#baz" # comment')).to eql("foo" => "bar#baz")
176 | end
177 |
178 | it "ignores comment lines" do
179 | expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql("foo" => "bar")
180 | end
181 |
182 | it "ignores commented out variables" do
183 | expect(env("# HELLO=world\n")).to eql({})
184 | end
185 |
186 | it "ignores comment" do
187 | expect(env("# Uncomment to activate:\n")).to eql({})
188 | end
189 |
190 | it "includes variables without values" do
191 | input = 'DATABASE_PASSWORD=
192 | DATABASE_USERNAME=root
193 | DATABASE_HOST=/tmp/mysql.sock'
194 |
195 | output = {
196 | "DATABASE_PASSWORD" => "",
197 | "DATABASE_USERNAME" => "root",
198 | "DATABASE_HOST" => "/tmp/mysql.sock"
199 | }
200 |
201 | expect(env(input)).to eql(output)
202 | end
203 |
204 | it "parses # in quoted values" do
205 | expect(env('foo="ba#r"')).to eql("foo" => "ba#r")
206 | expect(env("foo='ba#r'")).to eql("foo" => "ba#r")
207 | end
208 |
209 | it "parses # in quoted values with following spaces" do
210 | expect(env('foo="ba#r" ')).to eql("foo" => "ba#r")
211 | expect(env("foo='ba#r' ")).to eql("foo" => "ba#r")
212 | end
213 |
214 | it "parses empty values" do
215 | expect(env("foo=")).to eql("foo" => "")
216 | end
217 |
218 | it "allows multi-line values in single quotes" do
219 | env_file = %(OPTION_A=first line
220 | export OPTION_B='line 1
221 | line 2
222 | line 3'
223 | OPTION_C="last line"
224 | OPTION_ESCAPED='line one
225 | this is \\'quoted\\'
226 | one more line')
227 |
228 | expected_result = {
229 | "OPTION_A" => "first line",
230 | "OPTION_B" => "line 1\nline 2\nline 3",
231 | "OPTION_C" => "last line",
232 | "OPTION_ESCAPED" => "line one\nthis is \\'quoted\\'\none more line"
233 | }
234 | expect(env(env_file)).to eql(expected_result)
235 | end
236 |
237 | it "allows multi-line values in double quotes" do
238 | env_file = %(OPTION_A=first line
239 | export OPTION_B="line 1
240 | line 2
241 | line 3"
242 | OPTION_C="last line"
243 | OPTION_ESCAPED="line one
244 | this is \\"quoted\\"
245 | one more line")
246 |
247 | expected_result = {
248 | "OPTION_A" => "first line",
249 | "OPTION_B" => "line 1\nline 2\nline 3",
250 | "OPTION_C" => "last line",
251 | "OPTION_ESCAPED" => "line one\nthis is \"quoted\"\none more line"
252 | }
253 | expect(env(env_file)).to eql(expected_result)
254 | end
255 |
256 | if RUBY_VERSION > "1.8.7"
257 | it "parses shell commands interpolated in $()" do
258 | expect(env("echo=$(echo hello)")).to eql("echo" => "hello")
259 | end
260 |
261 | it "allows balanced parentheses within interpolated shell commands" do
262 | expect(env('echo=$(echo "$(echo "$(echo "$(echo hello)")")")'))
263 | .to eql("echo" => "hello")
264 | end
265 |
266 | it "doesn't interpolate shell commands when escape says not to" do
267 | expect(env('echo=escaped-\$(echo hello)'))
268 | .to eql("echo" => "escaped-$(echo hello)")
269 | end
270 |
271 | it "is not thrown off by quotes in interpolated shell commands" do
272 | expect(env('interp=$(echo "Quotes won\'t be a problem")')["interp"])
273 | .to eql("Quotes won't be a problem")
274 | end
275 |
276 | it "supports carriage return" do
277 | expect(env("FOO=bar\rbaz=fbb")).to eql("FOO" => "bar", "baz" => "fbb")
278 | end
279 |
280 | it "supports carriage return combine with new line" do
281 | expect(env("FOO=bar\r\nbaz=fbb")).to eql("FOO" => "bar", "baz" => "fbb")
282 | end
283 |
284 | it "escapes carriage return in quoted strings" do
285 | expect(env('FOO="bar\rbaz"')).to eql("FOO" => "bar\\rbaz")
286 | end
287 |
288 | it "escape $ properly when no alphabets/numbers/_ are followed by it" do
289 | expect(env("FOO=\"bar\\$ \\$\\$\"")).to eql("FOO" => "bar$ $$")
290 | end
291 |
292 | # echo bar $ -> prints bar $ in the shell
293 | it "ignore $ when it is not escaped and no variable is followed by it" do
294 | expect(env("FOO=\"bar $ \"")).to eql("FOO" => "bar $ ")
295 | end
296 |
297 | # This functionality is not supported on JRuby or Rubinius
298 | if (!defined?(RUBY_ENGINE) || RUBY_ENGINE != "jruby") &&
299 | !defined?(Rubinius)
300 | it "substitutes shell variables within interpolated shell commands" do
301 | expect(env(%(VAR1=var1\ninterp=$(echo "VAR1 is $VAR1")))["interp"])
302 | .to eql("VAR1 is var1")
303 | end
304 | end
305 | end
306 |
307 | it "returns existing value for redefined variable" do
308 | ENV["FOO"] = "existing"
309 | expect(env("FOO=bar")).to eql("FOO" => "existing")
310 | end
311 | end
312 |
--------------------------------------------------------------------------------
/spec/dotenv/rails_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "rails"
3 | require "dotenv/rails"
4 |
5 | describe Dotenv::Rails do
6 | let(:log_output) { StringIO.new }
7 | let(:application) do
8 | log_output = self.log_output
9 | Class.new(Rails::Application) do
10 | config.load_defaults Rails::VERSION::STRING.to_f
11 | config.eager_load = false
12 | config.logger = ActiveSupport::Logger.new(log_output)
13 | config.root = fixture_path
14 |
15 | # Remove method fails since app is reloaded for each test
16 | config.active_support.remove_deprecated_time_with_zone_name = false
17 | end.instance
18 | end
19 |
20 | around do |example|
21 | # These get frozen after the app initializes
22 | autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
23 | autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
24 |
25 | # Run in fixtures directory
26 | Dir.chdir(fixture_path) { example.run }
27 | ensure
28 | # Restore autoload paths to unfrozen state
29 | ActiveSupport::Dependencies.autoload_paths = autoload_paths
30 | ActiveSupport::Dependencies.autoload_once_paths = autoload_once_paths
31 | end
32 |
33 | before do
34 | Rails.env = "test"
35 | Rails.application = nil
36 | Rails.logger = nil
37 |
38 | begin
39 | # Remove the singleton instance if it exists
40 | Dotenv::Rails.remove_instance_variable(:@instance)
41 | rescue
42 | nil
43 | end
44 | end
45 |
46 | describe "files" do
47 | it "loads files for development environment" do
48 | Rails.env = "development"
49 |
50 | expect(Dotenv::Rails.files).to eql(
51 | [
52 | ".env.development.local",
53 | ".env.local",
54 | ".env.development",
55 | ".env"
56 | ]
57 | )
58 | end
59 |
60 | it "does not load .env.local in test rails environment" do
61 | Rails.env = "test"
62 | expect(Dotenv::Rails.files).to eql(
63 | [
64 | ".env.test.local",
65 | ".env.test",
66 | ".env"
67 | ]
68 | )
69 | end
70 |
71 | it "can be modified in place" do
72 | Dotenv::Rails.files << ".env.shared"
73 | expect(Dotenv::Rails.files.last).to eq(".env.shared")
74 | end
75 | end
76 |
77 | it "watches other loaded files with Spring" do
78 | stub_spring(load_watcher: true)
79 | application.initialize!
80 | path = fixture_path("plain.env")
81 | Dotenv.load(path)
82 | expect(Spring.watcher).to include(path.to_s)
83 | end
84 |
85 | it "doesn't raise an error if Spring.watch is not defined" do
86 | stub_spring(load_watcher: false)
87 |
88 | expect {
89 | application.initialize!
90 | }.to_not raise_error
91 | end
92 |
93 | context "before_configuration" do
94 | it "calls #load" do
95 | expect(Dotenv::Rails.instance).to receive(:load)
96 | ActiveSupport.run_load_hooks(:before_configuration)
97 | end
98 | end
99 |
100 | context "load" do
101 | subject { application.initialize! }
102 |
103 | it "watches .env with Spring" do
104 | stub_spring(load_watcher: true)
105 | subject
106 | expect(Spring.watcher).to include(fixture_path(".env").to_s)
107 | end
108 |
109 | it "loads .env.test before .env" do
110 | subject
111 | expect(ENV["DOTENV"]).to eql("test")
112 | end
113 |
114 | it "loads configured files" do
115 | Dotenv::Rails.files = [fixture_path("plain.env")]
116 | expect { subject }.to change { ENV["PLAIN"] }.from(nil).to("true")
117 | end
118 |
119 | it "loads file relative to Rails.root" do
120 | allow(Rails).to receive(:root).and_return(Pathname.new("/tmp"))
121 | Dotenv::Rails.files = [".env"]
122 | expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false})
123 | subject
124 | end
125 |
126 | it "returns absolute paths unchanged" do
127 | Dotenv::Rails.files = ["/tmp/.env"]
128 | expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false})
129 | subject
130 | end
131 |
132 | context "with overwrite = true" do
133 | before { Dotenv::Rails.overwrite = true }
134 |
135 | it "overwrites .env with .env.test" do
136 | subject
137 | expect(ENV["DOTENV"]).to eql("test")
138 | end
139 |
140 | it "overwrites any existing ENV variables" do
141 | ENV["DOTENV"] = "predefined"
142 | expect { subject }.to(change { ENV["DOTENV"] }.from("predefined").to("test"))
143 | end
144 | end
145 | end
146 |
147 | describe "root" do
148 | it "returns Rails.root" do
149 | expect(Dotenv::Rails.root).to eql(Rails.root)
150 | end
151 |
152 | context "when Rails.root is nil" do
153 | before do
154 | allow(Rails).to receive(:root).and_return(nil)
155 | end
156 |
157 | it "falls back to RAILS_ROOT" do
158 | ENV["RAILS_ROOT"] = "/tmp"
159 | expect(Dotenv::Rails.root.to_s).to eql("/tmp")
160 | end
161 | end
162 | end
163 |
164 | describe "autorestore" do
165 | it "is loaded if RAILS_ENV=test" do
166 | expect(Dotenv::Rails.autorestore).to eq(true)
167 | expect(Dotenv::Rails.instance).to receive(:require).with("dotenv/autorestore")
168 | application.initialize!
169 | end
170 |
171 | it "is not loaded if RAILS_ENV=development" do
172 | Rails.env = "development"
173 | expect(Dotenv::Rails.autorestore).to eq(false)
174 | expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
175 | application.initialize!
176 | end
177 |
178 | it "is not loaded if autorestore set to false" do
179 | Dotenv::Rails.autorestore = false
180 | expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
181 | application.initialize!
182 | end
183 |
184 | it "is not loaded if ClimateControl is defined" do
185 | stub_const("ClimateControl", Module.new)
186 | expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
187 | application.initialize!
188 | end
189 |
190 | it "is not loaded if IceAge is defined" do
191 | stub_const("IceAge", Module.new)
192 | expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
193 | application.initialize!
194 | end
195 | end
196 |
197 | describe "logger" do
198 | it "replays to Rails.logger" do
199 | expect(Dotenv::Rails.logger).to be_a(Dotenv::ReplayLogger)
200 | Dotenv::Rails.logger.debug("test")
201 |
202 | application.initialize!
203 |
204 | expect(Dotenv::Rails.logger).not_to be_a(Dotenv::ReplayLogger)
205 | expect(log_output.string).to include("[dotenv] test")
206 | end
207 |
208 | it "does not replace custom logger" do
209 | logger = Logger.new(log_output)
210 |
211 | Dotenv::Rails.logger = logger
212 | application.initialize!
213 | expect(Dotenv::Rails.logger).to be(logger)
214 | end
215 | end
216 |
217 | def stub_spring(load_watcher: true)
218 | spring = Module.new do
219 | if load_watcher
220 | def self.watcher
221 | @watcher ||= Set.new
222 | end
223 |
224 | def self.watch(path)
225 | watcher.add path
226 | end
227 | end
228 | end
229 |
230 | stub_const "Spring", spring
231 | end
232 | end
233 |
--------------------------------------------------------------------------------
/spec/dotenv_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Dotenv do
4 | before do
5 | Dir.chdir(File.expand_path("../fixtures", __FILE__))
6 | end
7 |
8 | shared_examples "load" do
9 | context "with no args" do
10 | let(:env_files) { [] }
11 |
12 | it "defaults to .env" do
13 | expect(Dotenv::Environment).to receive(:new).with(expand(".env"), anything).and_call_original
14 | subject
15 | end
16 | end
17 |
18 | context "with a tilde path" do
19 | let(:env_files) { ["~/.env"] }
20 |
21 | it "expands the path" do
22 | expected = expand("~/.env")
23 | allow(File).to receive(:exist?) { |arg| arg == expected }
24 | expect(Dotenv::Environment).to receive(:new).with(expected, anything)
25 | .and_return(Dotenv::Environment.new(".env"))
26 | subject
27 | end
28 | end
29 |
30 | context "with multiple files" do
31 | let(:env_files) { [".env", fixture_path("plain.env")] }
32 |
33 | let(:expected) do
34 | {"OPTION_A" => "1",
35 | "OPTION_B" => "2",
36 | "OPTION_C" => "3",
37 | "OPTION_D" => "4",
38 | "OPTION_E" => "5",
39 | "PLAIN" => "true",
40 | "DOTENV" => "true"}
41 | end
42 |
43 | it "loads all files" do
44 | subject
45 | expected.each do |key, value|
46 | expect(ENV[key]).to eq(value)
47 | end
48 | end
49 |
50 | it "returns hash of loaded variables" do
51 | expect(subject).to eq(expected)
52 | end
53 |
54 | it "does not return unchanged variables" do
55 | ENV["OPTION_A"] = "1"
56 | expect(subject).not_to have_key("OPTION_A")
57 | end
58 | end
59 | end
60 |
61 | shared_examples "overwrite" do
62 | it_behaves_like "load"
63 |
64 | context "with multiple files" do
65 | let(:env_files) { [fixture_path("important.env"), fixture_path("plain.env")] }
66 |
67 | let(:expected) do
68 | {
69 | "OPTION_A" => "abc",
70 | "OPTION_B" => "2",
71 | "OPTION_C" => "3",
72 | "OPTION_D" => "4",
73 | "OPTION_E" => "5",
74 | "PLAIN" => "false"
75 | }
76 | end
77 |
78 | it "respects the file importance order" do
79 | subject
80 | expected.each do |key, value|
81 | expect(ENV[key]).to eq(value)
82 | end
83 | end
84 | end
85 | end
86 |
87 | describe "load" do
88 | let(:env_files) { [] }
89 | let(:options) { {} }
90 | subject { Dotenv.load(*env_files, **options) }
91 |
92 | it_behaves_like "load"
93 |
94 | it "initializes the Environment with overwrite: false" do
95 | expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
96 | .and_call_original
97 | subject
98 | end
99 |
100 | context "when the file does not exist" do
101 | let(:env_files) { [".env_does_not_exist"] }
102 |
103 | it "fails silently" do
104 | expect { subject }.not_to raise_error
105 | end
106 |
107 | it "does not change ENV" do
108 | expect { subject }.not_to change { ENV.inspect }
109 | end
110 | end
111 |
112 | context "when the file is a directory" do
113 | let(:env_files) { [] }
114 |
115 | around do |example|
116 | Dir.mktmpdir do |dir|
117 | env_files.push dir
118 | example.run
119 | end
120 | end
121 |
122 | it "fails silently with ignore: true (default)" do
123 | expect { subject }.not_to raise_error
124 | end
125 |
126 | it "raises error with ignore: false" do
127 | options[:ignore] = false
128 | expect { subject }.to raise_error(/Is a directory/)
129 | end
130 | end
131 | end
132 |
133 | describe "load!" do
134 | let(:env_files) { [] }
135 | subject { Dotenv.load!(*env_files) }
136 |
137 | it_behaves_like "load"
138 |
139 | it "initializes Environment with overwrite: false" do
140 | expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
141 | .and_call_original
142 | subject
143 | end
144 |
145 | context "when one file exists and one does not" do
146 | let(:env_files) { [".env", ".env_does_not_exist"] }
147 |
148 | it "raises an Errno::ENOENT error" do
149 | expect { subject }.to raise_error(Errno::ENOENT)
150 | end
151 | end
152 | end
153 |
154 | describe "overwrite" do
155 | let(:env_files) { [fixture_path("plain.env")] }
156 | subject { Dotenv.overwrite(*env_files) }
157 | it_behaves_like "load"
158 | it_behaves_like "overwrite"
159 |
160 | it "initializes the Environment overwrite: true" do
161 | expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true)
162 | .and_call_original
163 | subject
164 | end
165 |
166 | context "when loading a file containing already set variables" do
167 | let(:env_files) { [fixture_path("plain.env")] }
168 |
169 | it "overwrites any existing ENV variables" do
170 | ENV["OPTION_A"] = "predefined"
171 |
172 | subject
173 |
174 | expect(ENV["OPTION_A"]).to eq("1")
175 | end
176 | end
177 |
178 | context "when the file does not exist" do
179 | let(:env_files) { [".env_does_not_exist"] }
180 |
181 | it "fails silently" do
182 | expect { subject }.not_to raise_error
183 | end
184 |
185 | it "does not change ENV" do
186 | expect { subject }.not_to change { ENV.inspect }
187 | end
188 | end
189 | end
190 |
191 | describe "overwrite!" do
192 | let(:env_files) { [fixture_path("plain.env")] }
193 | subject { Dotenv.overwrite!(*env_files) }
194 | it_behaves_like "load"
195 | it_behaves_like "overwrite"
196 |
197 | it "initializes the Environment with overwrite: true" do
198 | expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true)
199 | .and_call_original
200 | subject
201 | end
202 |
203 | context "when loading a file containing already set variables" do
204 | let(:env_files) { [fixture_path("plain.env")] }
205 |
206 | it "overwrites any existing ENV variables" do
207 | ENV["OPTION_A"] = "predefined"
208 | subject
209 | expect(ENV["OPTION_A"]).to eq("1")
210 | end
211 | end
212 |
213 | context "when one file exists and one does not" do
214 | let(:env_files) { [".env", ".env_does_not_exist"] }
215 |
216 | it "raises an Errno::ENOENT error" do
217 | expect { subject }.to raise_error(Errno::ENOENT)
218 | end
219 | end
220 | end
221 |
222 | describe "with an instrumenter" do
223 | let(:instrumenter) { double("instrumenter", instrument: {}) }
224 | before { Dotenv.instrumenter = instrumenter }
225 | after { Dotenv.instrumenter = nil }
226 |
227 | describe "load" do
228 | it "instruments if the file exists" do
229 | expect(instrumenter).to receive(:instrument) do |name, payload|
230 | expect(name).to eq("load.dotenv")
231 | expect(payload[:env]).to be_instance_of(Dotenv::Environment)
232 | {}
233 | end
234 | Dotenv.load
235 | end
236 |
237 | it "does not instrument if file does not exist" do
238 | expect(instrumenter).to receive(:instrument).never
239 | Dotenv.load ".doesnotexist"
240 | end
241 | end
242 | end
243 |
244 | describe "require keys" do
245 | let(:env_files) { [".env", fixture_path("bom.env")] }
246 |
247 | before { Dotenv.load(*env_files) }
248 |
249 | it "raises exception with not defined mandatory ENV keys" do
250 | expect { Dotenv.require_keys("BOM", "TEST") }.to raise_error(
251 | Dotenv::MissingKeys,
252 | 'Missing required configuration key: ["TEST"]'
253 | )
254 | end
255 | end
256 |
257 | describe "parse" do
258 | let(:env_files) { [] }
259 | subject { Dotenv.parse(*env_files) }
260 |
261 | context "with no args" do
262 | let(:env_files) { [] }
263 |
264 | it "defaults to .env" do
265 | expect(Dotenv::Environment).to receive(:new).with(expand(".env"),
266 | anything)
267 | subject
268 | end
269 | end
270 |
271 | context "with a tilde path" do
272 | let(:env_files) { ["~/.env"] }
273 |
274 | it "expands the path" do
275 | expected = expand("~/.env")
276 | allow(File).to receive(:exist?) { |arg| arg == expected }
277 | expect(Dotenv::Environment).to receive(:new).with(expected, anything)
278 | subject
279 | end
280 | end
281 |
282 | context "with multiple files" do
283 | let(:env_files) { [".env", fixture_path("plain.env")] }
284 |
285 | let(:expected) do
286 | {"OPTION_A" => "1",
287 | "OPTION_B" => "2",
288 | "OPTION_C" => "3",
289 | "OPTION_D" => "4",
290 | "OPTION_E" => "5",
291 | "PLAIN" => "true",
292 | "DOTENV" => "true"}
293 | end
294 |
295 | it "does not modify ENV" do
296 | subject
297 | expected.each do |key, _value|
298 | expect(ENV[key]).to be_nil
299 | end
300 | end
301 |
302 | it "returns hash of parsed key/value pairs" do
303 | expect(subject).to eq(expected)
304 | end
305 | end
306 |
307 | it "initializes the Environment with overwrite: false" do
308 | expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
309 | subject
310 | end
311 |
312 | context "when the file does not exist" do
313 | let(:env_files) { [".env_does_not_exist"] }
314 |
315 | it "fails silently" do
316 | expect { subject }.not_to raise_error
317 | expect(subject).to eq({})
318 | end
319 | end
320 | end
321 |
322 | describe "Unicode" do
323 | subject { fixture_path("bom.env") }
324 |
325 | it "loads a file with a Unicode BOM" do
326 | expect(Dotenv.load(subject)).to eql("BOM" => "UTF-8")
327 | end
328 |
329 | it "fixture file has UTF-8 BOM" do
330 | contents = File.binread(subject).force_encoding("UTF-8")
331 | expect(contents).to start_with("\xEF\xBB\xBF".force_encoding("UTF-8"))
332 | end
333 | end
334 |
335 | describe "restore" do
336 | it "restores previously saved snapshot" do
337 | ENV["MODIFIED"] = "true"
338 | Dotenv.restore # save was already called in setup
339 | expect(ENV["MODIFIED"]).to be_nil
340 | end
341 |
342 | it "raises an error in threads" do
343 | ENV["MODIFIED"] = "true"
344 | Thread.new do
345 | expect { Dotenv.restore }.to raise_error(ThreadError, /not thread safe/)
346 | end.join
347 | expect(ENV["MODIFIED"]).to eq("true")
348 | end
349 |
350 | it "is a noop if nil state provided" do
351 | expect { Dotenv.restore(nil) }.not_to raise_error
352 | end
353 |
354 | it "is a noop if no previously saved state" do
355 | # Clear state saved in setup
356 | expect(Dotenv.instance_variable_get(:@diff)).to be_instance_of(Dotenv::Diff)
357 | Dotenv.instance_variable_set(:@diff, nil)
358 | expect { Dotenv.restore }.not_to raise_error
359 | end
360 |
361 | it "can save and restore stubbed ENV" do
362 | stub_const("ENV", ENV.to_h.merge("STUBBED" => "1"))
363 | Dotenv.save
364 | ENV["MODIFIED"] = "1"
365 | Dotenv.restore
366 | expect(ENV["STUBBED"]).to eq("1")
367 | expect(ENV["MODIFED"]).to be(nil)
368 | end
369 | end
370 |
371 | describe "modify" do
372 | it "sets values for the block" do
373 | ENV["FOO"] = "initial"
374 |
375 | Dotenv.modify(FOO: "during", BAR: "baz") do
376 | expect(ENV["FOO"]).to eq("during")
377 | expect(ENV["BAR"]).to eq("baz")
378 | end
379 |
380 | expect(ENV["FOO"]).to eq("initial")
381 | expect(ENV).not_to have_key("BAR")
382 | end
383 | end
384 |
385 | describe "update" do
386 | it "sets new variables" do
387 | Dotenv.update({"OPTION_A" => "1"})
388 | expect(ENV["OPTION_A"]).to eq("1")
389 | end
390 |
391 | it "does not overwrite defined variables" do
392 | ENV["OPTION_A"] = "original"
393 | Dotenv.update({"OPTION_A" => "updated"})
394 | expect(ENV["OPTION_A"]).to eq("original")
395 | end
396 |
397 | context "with overwrite: true" do
398 | it "sets new variables" do
399 | Dotenv.update({"OPTION_A" => "1"}, overwrite: true)
400 | expect(ENV["OPTION_A"]).to eq("1")
401 | end
402 |
403 | it "overwrites defined variables" do
404 | ENV["OPTION_A"] = "original"
405 | Dotenv.update({"OPTION_A" => "updated"}, overwrite: true)
406 | expect(ENV["OPTION_A"]).to eq("updated")
407 | end
408 | end
409 | end
410 |
411 | def expand(path)
412 | File.expand_path path
413 | end
414 | end
415 |
--------------------------------------------------------------------------------
/spec/fixtures/.env:
--------------------------------------------------------------------------------
1 | DOTENV=true
2 |
--------------------------------------------------------------------------------
/spec/fixtures/.env.development:
--------------------------------------------------------------------------------
1 | FROM_RAILS_ENV=.env.development
2 | DOTENV=dev
3 |
--------------------------------------------------------------------------------
/spec/fixtures/.env.development.local:
--------------------------------------------------------------------------------
1 | FROM_RAILS_ENV=.env.test
2 | FROM_LOCAL=true
3 | DOTENV=development-local
4 |
--------------------------------------------------------------------------------
/spec/fixtures/.env.local:
--------------------------------------------------------------------------------
1 | FROM_LOCAL=true
2 | DOTENV=local
3 |
--------------------------------------------------------------------------------
/spec/fixtures/.env.test:
--------------------------------------------------------------------------------
1 | FROM_RAILS_ENV=.env.test
2 | DOTENV=test
3 |
--------------------------------------------------------------------------------
/spec/fixtures/bom.env:
--------------------------------------------------------------------------------
1 | BOM=UTF-8
--------------------------------------------------------------------------------
/spec/fixtures/exported.env:
--------------------------------------------------------------------------------
1 | export OPTION_A=2
2 | export OPTION_B='\n'
3 |
--------------------------------------------------------------------------------
/spec/fixtures/important.env:
--------------------------------------------------------------------------------
1 | PLAIN=false
2 | OPTION_A=abc
3 | OPTION_B=2
--------------------------------------------------------------------------------
/spec/fixtures/plain.env:
--------------------------------------------------------------------------------
1 | PLAIN=true
2 | OPTION_A=1
3 | OPTION_B=2
4 | OPTION_C= 3
5 | OPTION_D =4
6 | OPTION_E = 5
7 |
--------------------------------------------------------------------------------
/spec/fixtures/quoted.env:
--------------------------------------------------------------------------------
1 | QUOTED=true
2 | OPTION_A='1'
3 | OPTION_B='2'
4 | OPTION_C=''
5 | OPTION_D='\n'
6 | OPTION_E="1"
7 | OPTION_F="2"
8 | OPTION_G=""
9 | OPTION_H="\n"
10 |
--------------------------------------------------------------------------------
/spec/fixtures/yaml.env:
--------------------------------------------------------------------------------
1 | OPTION_A: 1
2 | OPTION_B: '2'
3 | OPTION_C: ''
4 | OPTION_D: '\n'
5 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "dotenv"
2 | require "dotenv/autorestore"
3 |
4 | def fixture_path(*parts)
5 | Pathname.new(__dir__).join("./fixtures", *parts)
6 | end
7 |
--------------------------------------------------------------------------------
/test/autorestore_test.rb:
--------------------------------------------------------------------------------
1 | require "active_support" # Rails 6.1 fails if this is not loaded
2 | require "active_support/test_case"
3 | require "minitest/autorun"
4 |
5 | require "dotenv"
6 | require "dotenv/autorestore"
7 |
8 | class AutorestoreTest < ActiveSupport::TestCase
9 | test "restores ENV between tests, part 1" do
10 | assert_nil ENV["DOTENV"], "ENV was not restored between tests"
11 | ENV["DOTENV"] = "1"
12 | end
13 |
14 | test "restores ENV between tests, part 2" do
15 | assert_nil ENV["DOTENV"], "ENV was not restored between tests"
16 | ENV["DOTENV"] = "2"
17 | end
18 | end
19 |
--------------------------------------------------------------------------------