├── .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 [![Gem Version](https://badge.fury.io/rb/dotenv.svg)](https://badge.fury.io/rb/dotenv) 2 | 3 | Shim to load environment variables from `.env` into `ENV` in *development*. 4 | 5 | 6 |
Dotenv is proud to be sponsored by:
7 | Stoked Seagull Software 8 |
Need help with a software project but don't know where to begin? Stoked Seagull can help.


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 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 |
PriorityEnvironment.gitignoreit?Notes
developmenttestproduction
highest.env.development.local.env.test.local.env.production.localYesEnvironment-specific local overrides
2nd.env.localN/A.env.localYesLocal overrides
3rd.env.development.env.test.env.productionNoShared environment-specific variables
last.env.env.envMaybeShared for all environments
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 | --------------------------------------------------------------------------------