Repository: bkeepers/dotenv Branch: main Commit: 34156bf400cd Files: 53 Total size: 79.2 KB Directory structure: gitextract_7mu46_o2/ ├── .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/ │ │ ├── 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 │ ├── dotenv-rails.rb │ └── dotenv.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/ │ │ ├── bom.env │ │ ├── exported.env │ │ ├── important.env │ │ ├── plain.env │ │ ├── quoted.env │ │ └── yaml.env │ └── spec_helper.rb └── test/ └── autorestore_test.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "bundler" directory: "/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/funding.yml ================================================ github: [bkeepers] ================================================ FILE: .github/issue_template.md ================================================ ### Steps to reproduce Tell us how to reproduce the issue. Show how you included dotenv (Gemfile). Paste your env using: ```bash $ env | grep MYVARIABLETOSHOW ``` **REMOVE ANY SENSITIVE INFORMATION FROM YOUR OUTPUT** ### Expected behavior Tell us what should happen ### Actual behavior Tell us what happens instead ### System configuration **dotenv version**: **Rails version**: **Ruby version**: ================================================ FILE: .github/stale.yml ================================================ # Number of days of inactivity before an issue becomes stale daysUntilStale: 60 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - pinned - security # Label to use when marking an issue as stale staleLabel: wontfix # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [main] pull_request: schedule: - cron: "0 0 * * *" # Once/day jobs: versions: name: Get latest versions runs-on: ubuntu-latest strategy: matrix: product: ["ruby", "rails"] outputs: ruby: ${{ steps.supported.outputs.ruby }} rails: ${{ steps.supported.outputs.rails }} steps: - id: supported run: | product="${{ matrix.product }}" data=$(curl https://endoflife.date/api/$product.json) supported=$(echo $data | jq '[.[] | select(.eol > (now | strftime("%Y-%m-%d")))]') echo "${product}=$(echo $supported | jq -c 'map(.latest)')" >> $GITHUB_OUTPUT test: needs: versions runs-on: ubuntu-latest name: Test on Ruby ${{ matrix.ruby }} and Rails ${{ matrix.rails }} strategy: fail-fast: false matrix: ruby: ${{ fromJSON(needs.versions.outputs.ruby) }} rails: ${{ fromJSON(needs.versions.outputs.rails) }} env: RAILS_VERSION: ${{ matrix.rails }} steps: - name: Check out repository code uses: actions/checkout@v6 - name: Set up Ruby id: setup-ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true ruby-version: ${{ matrix.ruby }} continue-on-error: true - name: Incompatible Versions if: steps.setup-ruby.outcome == 'failure' run: echo "Ruby ${{ matrix.ruby }} is not supported with Rails ${{ matrix.rails }}" - name: Run Rake if: steps.setup-ruby.outcome != 'failure' run: bundle exec rake ================================================ FILE: .github/workflows/release.yml ================================================ name: Publish Gem on: release: types: [published] jobs: build: runs-on: ubuntu-latest permissions: id-token: write # mandatory for trusted publishing contents: write # required for `rake release` to push the release tag steps: - uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: bundler-cache: true ruby-version: ruby - uses: rubygems/release-gem@v1 ================================================ FILE: .gitignore ================================================ *.gem *.rbc .bundle .config .ruby-version .yardoc Gemfile.lock tmp vendor .DS_Store ================================================ FILE: .standard.yml ================================================ ruby_version: 3.0 ignore: - lib/dotenv/parser.rb: - Lint/InheritException ================================================ FILE: Changelog.md ================================================ See [Releases](https://github.com/bkeepers/dotenv/releases) for the latest releases and changelogs. [View older releases](https://github.com/bkeepers/dotenv/blob/2840d9c4085a398cbde9f164465515b01c26a402/Changelog.md) ================================================ FILE: Gemfile ================================================ source "https://rubygems.org" gemspec name: "dotenv" gemspec name: "dotenv-rails" gem "railties", "~> #{ENV["RAILS_VERSION"] || "7.1"}" gem "benchmark-ips" gem "stackprof" group :guard do gem "guard-rspec" gem "guard-bundler" gem "rb-fsevent" end ================================================ FILE: Guardfile ================================================ guard "bundler" do watch("Gemfile") end guard "rspec", cmd: "bundle exec rspec" do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^spec/spec_helper.rb$}) { "spec" } watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } end ================================================ FILE: LICENSE ================================================ Copyright (c) 2012 Brandon Keepers MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: OWNERS ================================================ # This project is maintained by: @bkeepers # For more information on the OWNERS file, see: # https://github.com/bkeepers/OWNERS ================================================ FILE: README.md ================================================ # dotenv [![Gem Version](https://badge.fury.io/rb/dotenv.svg)](https://badge.fury.io/rb/dotenv) Shim to load environment variables from `.env` into `ENV` in *development*. 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. 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. ## Installation Add this line to the top of your application's Gemfile and run `bundle install`: ```ruby gem 'dotenv', groups: [:development, :test] ``` ## Usage Add your application configuration to your `.env` file in the root of your project: ```shell S3_BUCKET=YOURS3BUCKET SECRET_KEY=YOURSECRETKEYGOESHERE ``` Whenever your application loads, these variables will be available in `ENV`: ```ruby config.fog_directory = ENV['S3_BUCKET'] ``` See the [API Docs](https://rubydoc.info/github/bkeepers/dotenv/main) for more. ### Rails Dotenv will automatically load when your Rails app boots. See [Customizing Rails](#customizing-rails) to change which files are loaded and when. ### Sinatra / Ruby Load Dotenv as early as possible in your application bootstrap process: ```ruby require 'dotenv/load' # or require 'dotenv' Dotenv.load ``` 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. Existing environment variables will not be overwritten unless you set `overwrite: true`. ```ruby require 'dotenv' Dotenv.load('file1.env', 'file2.env') ``` ### Autorestore in tests 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`. 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). To use this behavior outside of a Rails app, just `require "dotenv/autorestore"` in your test suite. 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. ### Rake To ensure `.env` is loaded in rake, load the tasks: ```ruby require 'dotenv/tasks' task mytask: :dotenv do # things that require .env end ``` ### CLI You can use the `dotenv` executable load `.env` before launching your application: ```console $ dotenv ./script.rb ``` 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. ```console $ dotenv -f ".env.local,.env" ./script.rb ``` 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. ```console $ dotenv -i -f ".env.local,.env" ./script.rb ``` ### Load Order 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`. ```ruby gem 'dotenv', require: 'dotenv/load' gem 'gem-that-requires-env-variables' ``` ### Customizing Rails Dotenv will load the following files depending on `RAILS_ENV`, with the first file having the highest precedence, and `.env` having the lowest precedence:
Priority Environment .gitignoreit? Notes
development test production
highest .env.development.local .env.test.local .env.production.local Yes Environment-specific local overrides
2nd .env.local N/A .env.local Yes Local overrides
3rd .env.development .env.test .env.production No Shared environment-specific variables
last .env .env .env Maybe Shared for all environments
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` ```ruby # config/application.rb Bundler.require(*Rails.groups) # Load .env.local in test Dotenv::Rails.files.unshift(".env.local") if ENV["RAILS_ENV"] == "test" module YourApp class Application < Rails::Application # ... end end ``` Available options: * `Dotenv::Rails.files` - list of files to be loaded, in order of precedence. * `Dotenv::Rails.overwrite` - Overwrite existing `ENV` variables with contents of `.env*` files * `Dotenv::Rails.logger` - The logger to use for dotenv's logging. Defaults to `Rails.logger` * `Dotenv::Rails.autorestore` - Enable or disable [autorestore](#autorestore-in-tests) ### Multi-line values Multi-line values with line breaks must be surrounded with double quotes. ```shell PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- ... HkVN9... ... -----END DSA PRIVATE KEY-----" ``` 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`: ```shell DOTENV_LINEBREAK_MODE=legacy PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9...\n-----END DSA PRIVATE KEY-----\n" ``` ### Command Substitution You need to add the output of a command in one of your variables? Simply add it with `$(your_command)`: ```shell DATABASE_URL="postgres://$(whoami)@localhost/my_database" ``` ### Variable Substitution 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. ```shell DATABASE_URL="postgres://${USER}@localhost/my_database" ``` If a value contains a `$` and it is not intended to be a variable, wrap it in single quotes. ```shell PASSWORD='pas$word' ``` ### Comments Comments may be added to your file as such: ```shell # This is a comment SECRET_KEY=YOURSECRETKEYGOESHERE # comment SECRET_HASH="something-with-a-#-hash" ``` ### Exports For compatability, you may also add `export` in front of each line so you can `source` the file in bash: ```shell export S3_BUCKET=YOURS3BUCKET export SECRET_KEY=YOURSECRETKEYGOESHERE ``` ### Required Keys If a particular configuration value is required but not set, it's appropriate to raise an error. To require configuration keys: ```ruby # config/initializers/dotenv.rb Dotenv.require_keys("SERVICE_APP_ID", "SERVICE_KEY", "SERVICE_SECRET") ``` 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. ### Parsing To parse a list of env files for programmatic inspection without modifying the ENV: ```ruby Dotenv.parse(".env.local", ".env") # => {'S3_BUCKET' => 'YOURS3BUCKET', 'SECRET_KEY' => 'YOURSECRETKEYGOESHERE', ...} ``` This method returns a hash of the ENV var name/value pairs. ### Templates You can use the `-t` or `--template` flag on the dotenv cli to create a template of your `.env` file. ```console $ dotenv -t .env ``` A template will be created in your working directory named `{FILENAME}.template`. So in the above example, it would create a `.env.template` file. The template will contain all the environment variables in your `.env` file but with their values set to the variable names. ```shell # .env S3_BUCKET=YOURS3BUCKET SECRET_KEY=YOURSECRETKEYGOESHERE ``` Would become ```shell # .env.template S3_BUCKET=S3_BUCKET SECRET_KEY=SECRET_KEY ``` ## Frequently Answered Questions ### Can I use dotenv in production? 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. 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`. 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.`. ### Should I commit my .env file? 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. 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. ### Why is it not overwriting existing `ENV` variables? 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`. To warn when a value was not overwritten (e.g. to make users aware of this gotcha), use `Dotenv.load files, overwrite: :warn`. You can also use the `-o` or `--overwrite` flag on the dotenv cli to overwrite existing `ENV` variables. ```console $ dotenv -o -f ".env.local,.env" ``` ## Contributing 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). 1. Fork it 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Added some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create new Pull Request ================================================ FILE: Rakefile ================================================ #!/usr/bin/env rake require "bundler/gem_helper" require "rspec/core/rake_task" require "rake/testtask" require "standard/rake" namespace "dotenv" do Bundler::GemHelper.install_tasks name: "dotenv" end class DotenvRailsGemHelper < Bundler::GemHelper def guard_already_tagged # noop end def tag_version # noop end end namespace "dotenv-rails" do DotenvRailsGemHelper.install_tasks name: "dotenv-rails" end task build: ["dotenv:build", "dotenv-rails:build"] task install: ["dotenv:install", "dotenv-rails:install"] task release: ["dotenv:release", "dotenv-rails:release"] desc "Run all specs" RSpec::Core::RakeTask.new(:spec) do |t| t.rspec_opts = %w[--color] t.verbose = false end Rake::TestTask.new do |t| t.test_files = Dir["test/**/*_test.rb"] end task default: [:spec, :test, :standard] ================================================ FILE: benchmark/parse_ips.rb ================================================ require "bundler/setup" require "dotenv" require "benchmark/ips" require "tempfile" f = Tempfile.create("benchmark_ips.env") 1000.times.map { |i| f.puts "VAR_#{i}=#{i}" } f.close Benchmark.ips do |x| x.report("parse, overwrite:false") { Dotenv.parse(f.path, overwrite: false) } x.report("parse, overwrite:true") { Dotenv.parse(f.path, overwrite: true) } end File.unlink(f.path) ================================================ FILE: benchmark/parse_profile.rb ================================================ require "bundler/setup" require "dotenv" require "stackprof" require "benchmark/ips" require "tempfile" f = Tempfile.create("benchmark_ips.env") 1000.times.map { |i| f.puts "VAR_#{i}=#{i}" } f.close profile = StackProf.run(mode: :wall, interval: 1_000) do 10_000.times do Dotenv.parse(f.path, overwrite: false) end end result = StackProf::Report.new(profile) puts result.print_text puts "\n\n\n" result.print_method(/Dotenv.parse/) File.unlink(f.path) ================================================ FILE: bin/dotenv ================================================ #!/usr/bin/env ruby require "dotenv/cli" Dotenv::CLI.new(ARGV).run ================================================ FILE: dotenv-rails.gemspec ================================================ require File.expand_path("../lib/dotenv/version", __FILE__) require "English" Gem::Specification.new "dotenv-rails", Dotenv::VERSION do |gem| gem.authors = ["Brandon Keepers"] gem.email = ["brandon@opensoul.org"] gem.description = gem.summary = "Autoload dotenv in Rails." gem.homepage = "https://github.com/bkeepers/dotenv" gem.license = "MIT" gem.files = `git ls-files lib | grep dotenv-rails.rb`.split("\n") + ["README.md", "LICENSE"] gem.add_dependency "dotenv", Dotenv::VERSION gem.add_dependency "railties", ">= 6.1" gem.add_development_dependency "spring" gem.metadata = { "changelog_uri" => "https://github.com/bkeepers/dotenv/releases", "funding_uri" => "https://github.com/sponsors/bkeepers" } end ================================================ FILE: dotenv.gemspec ================================================ require File.expand_path("../lib/dotenv/version", __FILE__) require "English" Gem::Specification.new "dotenv", Dotenv::VERSION do |gem| gem.authors = ["Brandon Keepers"] gem.email = ["brandon@opensoul.org"] gem.description = gem.summary = "Loads environment variables from `.env`." gem.homepage = "https://github.com/bkeepers/dotenv" gem.license = "MIT" gem.files = `git ls-files README.md LICENSE lib bin | grep -v dotenv-rails.rb`.split("\n") gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.add_development_dependency "rake" gem.add_development_dependency "rspec" gem.add_development_dependency "standard" gem.required_ruby_version = ">= 3.0" gem.metadata = { "changelog_uri" => "https://github.com/bkeepers/dotenv/releases", "funding_uri" => "https://github.com/sponsors/bkeepers" } end ================================================ FILE: lib/dotenv/autorestore.rb ================================================ # Automatically restore `ENV` to its original state after if defined?(RSpec.configure) RSpec.configure do |config| # Save ENV before the suite starts config.before(:suite) { Dotenv.save } # Restore ENV after each example config.after { Dotenv.restore } end end if defined?(ActiveSupport) ActiveSupport.on_load(:active_support_test_case) do ActiveSupport::TestCase.class_eval do # Save ENV before each test setup { Dotenv.save } # Restore ENV after each test teardown do Dotenv.restore rescue ThreadError => e # Restore will fail if running tests in parallel. warn e.message warn "Set `config.dotenv.autorestore = false` in `config/initializers/test.rb`" if defined?(Dotenv::Rails) end end end end ================================================ FILE: lib/dotenv/cli.rb ================================================ require "dotenv" require "dotenv/version" require "dotenv/template" require "optparse" module Dotenv # The `dotenv` command line interface. Run `$ dotenv --help` to see usage. class CLI < OptionParser attr_reader :argv, :filenames, :overwrite def initialize(argv = []) @argv = argv.dup @filenames = [] @ignore = false @overwrite = false super("Usage: dotenv [options]") separator "" on("-f FILES", Array, "List of env files to parse") do |list| @filenames = list end on("-i", "--ignore", "ignore missing env files") do @ignore = true end on("-o", "--overwrite", "overwrite existing ENV variables") do @overwrite = true end on("--overload") { @overwrite = true } on("-h", "--help", "Display help") do puts self exit end on("-v", "--version", "Show version") do puts "dotenv #{Dotenv::VERSION}" exit end on("-t", "--template=FILE", "Create a template env file") do |file| template = Dotenv::EnvTemplate.new(file) template.create_template end order!(@argv) end def run Dotenv.load(*@filenames, overwrite: @overwrite, ignore: @ignore) rescue Errno::ENOENT => e abort e.message else exec(*@argv) unless @argv.empty? end end end ================================================ FILE: lib/dotenv/diff.rb ================================================ module Dotenv # A diff between multiple states of ENV. class Diff # The initial state attr_reader :a # The final or current state attr_reader :b # Create a new diff. If given a block, the state of ENV after the block will be preserved as # the final state for comparison. Otherwise, the current ENV will be the final state. # # @param a [Hash] the initial state, defaults to a snapshot of current ENV # @param b [Hash] the final state, defaults to the current ENV # @yield [diff] a block to execute before recording the final state def initialize(a: snapshot, b: ENV, &block) @a, @b = a, b block&.call self ensure @b = snapshot if block end # Return a Hash of keys added with their new values def added b.slice(*(b.keys - a.keys)) end # Returns a Hash of keys removed with their previous values def removed a.slice(*(a.keys - b.keys)) end # Returns of Hash of keys changed with an array of their previous and new values def changed (b.slice(*a.keys).to_a - a.to_a).map do |(k, v)| [k, [a[k], v]] end.to_h end # Returns a Hash of all added, changed, and removed keys and their new values def env b.slice(*(added.keys + changed.keys)).merge(removed.transform_values { |v| nil }) end # Returns true if any keys were added, removed, or changed def any? [added, removed, changed].any?(&:any?) end private def snapshot # `dup` should not be required here, but some people use `stub_const` to replace ENV with # a `Hash`. This ensures that we get a frozen copy of that instead of freezing the original. # https://github.com/bkeepers/dotenv/issues/482 ENV.to_h.dup.freeze end end end ================================================ FILE: lib/dotenv/environment.rb ================================================ module Dotenv # A `.env` file that will be read and parsed into a Hash class Environment < Hash attr_reader :filename, :overwrite # Create a new Environment # # @param filename [String] the path to the file to read # @param overwrite [Boolean] whether the parser should assume existing values will be overwritten def initialize(filename, overwrite: false) super() @filename = filename @overwrite = overwrite load end def load update Parser.call(read, overwrite: overwrite) end def read File.open(@filename, "rb:bom|utf-8", &:read) end end end ================================================ FILE: lib/dotenv/load.rb ================================================ require "dotenv" defined?(Dotenv::Rails) ? Dotenv::Rails.load : Dotenv.load ================================================ FILE: lib/dotenv/log_subscriber.rb ================================================ require "active_support/log_subscriber" module Dotenv # Logs instrumented events # # Usage: # require "active_support/notifications" # require "dotenv/log_subscriber" # Dotenv.instrumenter = ActiveSupport::Notifications # class LogSubscriber < ActiveSupport::LogSubscriber attach_to :dotenv def logger Dotenv::Rails.logger end def load(event) env = event.payload[:env] info "Loaded #{color_filename(env.filename)}" end def update(event) diff = event.payload[:diff] changed = diff.env.keys.map { |key| color_var(key) } debug "Set #{changed.join(", ")}" if diff.any? end def save(event) info "Saved a snapshot of #{color_env_constant}" end def restore(event) diff = event.payload[:diff] removed = diff.removed.keys.map { |key| color(key, :RED) } restored = (diff.changed.keys + diff.added.keys).map { |key| color_var(key) } if removed.any? || restored.any? info "Restored snapshot of #{color_env_constant}" debug "Unset #{removed.join(", ")}" if removed.any? debug "Restored #{restored.join(", ")}" if restored.any? end end private def color_filename(filename) color(Pathname.new(filename).relative_path_from(Dotenv::Rails.root.to_s).to_s, :YELLOW) end def color_var(name) color(name, :CYAN) end def color_env_constant color("ENV", :GREEN) end end end ================================================ FILE: lib/dotenv/missing_keys.rb ================================================ module Dotenv class Error < StandardError; end class MissingKeys < Error # :nodoc: def initialize(keys) key_word = "key#{"s" if keys.size > 1}" super("Missing required configuration #{key_word}: #{keys.inspect}") end end end ================================================ FILE: lib/dotenv/parser.rb ================================================ require "dotenv/substitutions/variable" require "dotenv/substitutions/command" if RUBY_VERSION > "1.8.7" module Dotenv # Error raised when encountering a syntax error while parsing a .env file. class FormatError < SyntaxError; end # Parses the `.env` file format into key/value pairs. # It allows for variable substitutions, command substitutions, and exporting of variables. class Parser @substitutions = [ Dotenv::Substitutions::Command, Dotenv::Substitutions::Variable ] LINE = / (?:^|\A) # beginning of line \s* # leading whitespace (?export\s+)? # optional export (?[\w.]+) # key (?: # optional separator and value (?:\s*=\s*?|:\s+?) # separator (? # optional value begin \s*'(?:\\'|[^'])*' # single quoted value | # or \s*"(?:\\"|[^"])*" # double quoted value | # or [^\#\n]+ # unquoted value )? # value end )? # separator and value end \s* # trailing whitespace (?:\#.*)? # optional comment (?:$|\z) # end of line /x QUOTED_STRING = /\A(['"])(.*)\1\z/m class << self attr_reader :substitutions def call(...) new(...).call end end def initialize(string, overwrite: false) # Convert line breaks to same format @string = string.gsub(/\r\n?/, "\n") @hash = {} @overwrite = overwrite end def call @string.scan(LINE) do match = $LAST_MATCH_INFO if existing?(match[:key]) # Use value from already defined variable @hash[match[:key]] = ENV[match[:key]] elsif match[:export] && !match[:value] # Check for exported variable with no value if !@hash.member?(match[:key]) raise FormatError, "Line #{match.to_s.inspect} has an unset variable" end else @hash[match[:key]] = parse_value(match[:value] || "") end end @hash end private # Determine if a variable is already defined and should not be overwritten. def existing?(key) !@overwrite && key != "DOTENV_LINEBREAK_MODE" && ENV.key?(key) end def parse_value(value) # Remove surrounding quotes value = value.strip.sub(QUOTED_STRING, '\2') maybe_quote = Regexp.last_match(1) # Expand new lines in double quoted values value = expand_newlines(value) if maybe_quote == '"' # Unescape characters and performs substitutions unless value is single quoted if maybe_quote != "'" value = unescape_characters(value) self.class.substitutions.each { |proc| value = proc.call(value, @hash) } end value end def unescape_characters(value) value.gsub(/\\([^$])/, '\1') end def expand_newlines(value) if (@hash["DOTENV_LINEBREAK_MODE"] || ENV["DOTENV_LINEBREAK_MODE"]) == "legacy" value.gsub('\n', "\n").gsub('\r', "\r") else value.gsub('\n', "\\\\\\n").gsub('\r', "\\\\\\r") end end end end ================================================ FILE: lib/dotenv/rails-now.rb ================================================ # 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`. # # gem "dotenv", require: "dotenv/load" # gem "gem-that-requires-env-variables" # require "dotenv/load" warn '[DEPRECATION] `require "dotenv/rails-now"` is deprecated. Use `require "dotenv/load"` instead.', caller(1..1).first ================================================ FILE: lib/dotenv/rails.rb ================================================ # Since rubygems doesn't support optional dependencies, we have to manually check unless Gem::Requirement.new(">= 6.1").satisfied_by?(Gem::Version.new(Rails.version)) warn "dotenv 3.0 only supports Rails 6.1 or later. Use dotenv ~> 2.0." return end require "dotenv/replay_logger" require "dotenv/log_subscriber" Dotenv.instrumenter = ActiveSupport::Notifications # Watch all loaded env files with Spring ActiveSupport::Notifications.subscribe("load.dotenv") do |*args| if defined?(Spring) && Spring.respond_to?(:watch) event = ActiveSupport::Notifications::Event.new(*args) Spring.watch event.payload[:env].filename if Rails.application end end module Dotenv # Rails integration for using Dotenv to load ENV variables from a file class Rails < ::Rails::Railtie delegate :files, :files=, :overwrite, :overwrite=, :autorestore, :autorestore=, :logger, to: "config.dotenv" def initialize super config.dotenv = ActiveSupport::OrderedOptions.new.update( # Rails.logger is not available yet, so we'll save log messages and replay them when it is logger: Dotenv::ReplayLogger.new, overwrite: false, files: [ ".env.#{env}.local", (".env.local" unless env.test?), ".env.#{env}", ".env" ].compact, autorestore: env.test? && !defined?(ClimateControl) && !defined?(IceAge) ) end # Public: Load dotenv # # This will get called during the `before_configuration` callback, but you # can manually call `Dotenv::Rails.load` if you needed it sooner. def load Dotenv.load(*files.map { |file| root.join(file).to_s }, overwrite: overwrite) end def overload deprecator.warn("Dotenv::Rails.overload is deprecated. Set `Dotenv::Rails.overwrite = true` and call Dotenv::Rails.load instead.") Dotenv.load(*files.map { |file| root.join(file).to_s }, overwrite: true) end # Internal: `Rails.root` is nil in Rails 4.1 before the application is # initialized, so this falls back to the `RAILS_ROOT` environment variable, # or the current working directory. def root ::Rails.root || Pathname.new(ENV["RAILS_ROOT"] || Dir.pwd) end # Set a new logger and replay logs def logger=(new_logger) logger.replay new_logger if logger.is_a?(ReplayLogger) config.dotenv.logger = new_logger end # The current environment that the app is running in. # # When running `rake`, the Rails application is initialized in development, so we have to # check which rake tasks are being run to determine the environment. # # See https://github.com/bkeepers/dotenv/issues/219 def env @env ||= if defined?(Rake.application) && Rake.application.top_level_tasks.grep(TEST_RAKE_TASKS).any? env = Rake.application.options.show_tasks ? "development" : "test" ActiveSupport::EnvironmentInquirer.new(env) else ::Rails.env end end TEST_RAKE_TASKS = /^(default$|test(:|$)|parallel:spec|spec(:|$))/ def deprecator # :nodoc: @deprecator ||= ActiveSupport::Deprecation.new end # Rails uses `#method_missing` to delegate all class methods to the # instance, which means `Kernel#load` gets called here. We don't want that. def self.load instance.load end initializer "dotenv", after: :initialize_logger do |app| if logger.is_a?(ReplayLogger) self.logger = ActiveSupport::TaggedLogging.new(::Rails.logger).tagged("dotenv") end end initializer "dotenv.deprecator" do |app| app.deprecators[:dotenv] = deprecator if app.respond_to?(:deprecators) end initializer "dotenv.autorestore" do |app| require "dotenv/autorestore" if autorestore end config.before_configuration { load } end Railtie = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Dotenv::Railtie", "Dotenv::Rails", Dotenv::Rails.deprecator) end ================================================ FILE: lib/dotenv/replay_logger.rb ================================================ module Dotenv # A logger that can be used before the apps real logger is initialized. class ReplayLogger < Logger def initialize super(nil) # Doesn't matter what this is, it won't be used. @logs = [] end # Override the add method to store logs so we can replay them to a real logger later. def add(*args, &block) @logs.push([args, block]) end # Replay the store logs to a real logger. def replay(logger) @logs.each { |args, block| logger.add(*args, &block) } @logs.clear end end end ================================================ FILE: lib/dotenv/substitutions/command.rb ================================================ require "English" module Dotenv module Substitutions # Substitute shell commands in a value. # # SHA=$(git rev-parse HEAD) # module Command class << self INTERPOLATED_SHELL_COMMAND = / (?\\)? # is it escaped with a backslash? \$ # literal $ (? # collect command content for eval \( # require opening paren (?:[^()]|\g)+ # allow any number of non-parens, or balanced # parens (by nesting the expression # recursively) \) # require closing paren ) /x def call(value, env) # Process interpolated shell commands value.gsub(INTERPOLATED_SHELL_COMMAND) do |*| # Eliminate opening and closing parentheses command = $LAST_MATCH_INFO[:cmd][1..-2] if $LAST_MATCH_INFO[:backslash] # Command is escaped, don't replace it. $LAST_MATCH_INFO[0][1..] else # Execute the command and return the value `#{Variable.call(command, env)}`.chomp end end end end end end end ================================================ FILE: lib/dotenv/substitutions/variable.rb ================================================ require "English" module Dotenv module Substitutions # Substitute variables in a value. # # HOST=example.com # URL="https://$HOST" # module Variable class << self VARIABLE = / (\\)? # is it escaped with a backslash? (\$) # literal $ (?!\() # shouldn't be followed by parenthesis \{? # allow brace wrapping ([A-Z0-9_]+)? # optional alpha nums \}? # closing brace /xi def call(value, env) value.gsub(VARIABLE) do |variable| match = $LAST_MATCH_INFO if match[1] == "\\" variable[1..] elsif match[3] env[match[3]] || ENV[match[3]] || "" else variable end end end end end end end ================================================ FILE: lib/dotenv/tasks.rb ================================================ desc "Load environment settings from .env" task :dotenv do require "dotenv" Dotenv.load end task environment: :dotenv ================================================ FILE: lib/dotenv/template.rb ================================================ module Dotenv EXPORT_COMMAND = "export ".freeze # Class for creating a template from a env file class EnvTemplate def initialize(env_file) @env_file = env_file end def create_template File.open(@env_file, "r") do |env_file| File.open("#{@env_file}.template", "w") do |env_template| env_file.each do |line| if is_comment?(line) env_template.puts line elsif (var = var_defined?(line)) if line.match(EXPORT_COMMAND) env_template.puts "export #{var}=#{var}" else env_template.puts "#{var}=#{var}" end elsif line_blank?(line) env_template.puts end end end end end private def is_comment?(line) line.strip.start_with?("#") end def var_defined?(line) match = Dotenv::Parser::LINE.match(line) match && match[:key] end def line_blank?(line) line.strip.length.zero? end end end ================================================ FILE: lib/dotenv/version.rb ================================================ module Dotenv VERSION = "3.2.0".freeze end ================================================ FILE: lib/dotenv-rails.rb ================================================ require "dotenv" ================================================ FILE: lib/dotenv.rb ================================================ require "dotenv/version" require "dotenv/parser" require "dotenv/environment" require "dotenv/missing_keys" require "dotenv/diff" # Shim to load environment variables from `.env files into `ENV`. module Dotenv extend self # An internal monitor to synchronize access to ENV in multi-threaded environments. SEMAPHORE = Monitor.new private_constant :SEMAPHORE attr_accessor :instrumenter # Loads environment variables from one or more `.env` files. See `#parse` for more details. def load(*filenames, overwrite: false, ignore: true) parse(*filenames, overwrite: overwrite, ignore: ignore) do |env| instrument(:load, env: env) do |payload| update(env, overwrite: overwrite) end end end # Same as `#load`, but raises Errno::ENOENT if any files don't exist def load!(*filenames) load(*filenames, ignore: false) end # same as `#load`, but will overwrite existing values in `ENV` def overwrite(*filenames) load(*filenames, overwrite: true) end alias_method :overload, :overwrite # same as `#overwrite`, but raises Errno::ENOENT if any files don't exist def overwrite!(*filenames) load(*filenames, overwrite: true, ignore: false) end alias_method :overload!, :overwrite! # Parses the given files, yielding for each file if a block is given. # # @param filenames [String, Array] Files to parse # @param overwrite [Boolean] Overwrite existing `ENV` values # @param ignore [Boolean] Ignore non-existent files # @param block [Proc] Block to yield for each parsed `Dotenv::Environment` # @return [Hash] parsed key/value pairs def parse(*filenames, overwrite: false, ignore: true, &block) filenames << ".env" if filenames.empty? filenames = filenames.reverse if overwrite filenames.reduce({}) do |hash, filename| begin env = Environment.new(File.expand_path(filename), overwrite: overwrite) env = block.call(env) if block rescue Errno::ENOENT, Errno::EISDIR raise unless ignore end hash.merge! env || {} end end # Save the current `ENV` to be restored later def save instrument(:save) do |payload| @diff = payload[:diff] = Dotenv::Diff.new end end # Restore `ENV` to a given state # # @param env [Hash] Hash of keys and values to restore, defaults to the last saved state # @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error. def restore(env = @diff&.a, safe: Thread.current == Thread.main) # No previously saved or provided state to restore return unless env diff = Dotenv::Diff.new(b: env) return unless diff.any? unless safe raise ThreadError, <<~EOE.tr("\n", " ") Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore this error. EOE end instrument(:restore, diff: diff) { ENV.replace(env) } end # Update `ENV` with the given hash of keys and values # # @param env [Hash] Hash of keys and values to set in `ENV` # @param overwrite [Boolean|:warn] Overwrite existing `ENV` values def update(env = {}, overwrite: false) instrument(:update) do |payload| diff = payload[:diff] = Dotenv::Diff.new do ENV.update(env.transform_keys(&:to_s)) do |key, old_value, new_value| # This block is called when a key exists. Return the new value if overwrite is true. case overwrite when :warn # not printing the value since that could be a secret warn "Warning: dotenv not overwriting ENV[#{key.inspect}]" old_value when true then new_value when false then old_value else raise ArgumentError, "Invalid value for overwrite: #{overwrite.inspect}" end end end diff.env end end # Modify `ENV` for the block and restore it to its previous state afterwards. # # Note that the block is synchronized to prevent concurrent modifications to `ENV`, # so multiple threads will be executed serially. # # @param env [Hash] Hash of keys and values to set in `ENV` def modify(env = {}, &block) SEMAPHORE.synchronize do diff = Dotenv::Diff.new update(env, overwrite: true) block.call ensure restore(diff.a, safe: true) end end def require_keys(*keys) missing_keys = keys.flatten - ::ENV.keys return if missing_keys.empty? raise MissingKeys, missing_keys end private def instrument(name, payload = {}, &block) if instrumenter instrumenter.instrument("#{name}.dotenv", payload, &block) else block&.call payload end end end require "dotenv/rails" if defined?(Rails::Railtie) ================================================ FILE: spec/dotenv/cli_spec.rb ================================================ require "spec_helper" require "dotenv/cli" describe "dotenv binary" do before do Dir.chdir(File.expand_path("../../fixtures", __FILE__)) end def run(*args) Dotenv::CLI.new(args).run end it "loads from .env by default" do expect(ENV).not_to have_key("DOTENV") run expect(ENV).to have_key("DOTENV") end it "loads from file specified by -f" do expect(ENV).not_to have_key("OPTION_A") run "-f", "plain.env" expect(ENV).to have_key("OPTION_A") end it "dies if file specified by -f doesn't exist" do expect do capture_output { run "-f", ".doesnotexist" } end.to raise_error(SystemExit, /No such file/) end it "ignores missing files when --ignore flag given" do expect do run "--ignore", "-f", ".doesnotexist" end.not_to raise_error end it "loads from multiple files specified by -f" do expect(ENV).not_to have_key("PLAIN") expect(ENV).not_to have_key("QUOTED") run "-f", "plain.env,quoted.env" expect(ENV).to have_key("PLAIN") expect(ENV).to have_key("QUOTED") end it "does not consume non-dotenv flags by accident" do cli = Dotenv::CLI.new(["-f", "plain.env", "foo", "--switch"]) expect(cli.filenames).to eql(["plain.env"]) expect(cli.argv).to eql(["foo", "--switch"]) end it "does not consume dotenv flags from subcommand" do cli = Dotenv::CLI.new(["foo", "-f", "something"]) expect(cli.filenames).to eql([]) expect(cli.argv).to eql(["foo", "-f", "something"]) end it "does not mess with quoted args" do cli = Dotenv::CLI.new(["foo something"]) expect(cli.filenames).to eql([]) expect(cli.argv).to eql(["foo something"]) end describe "templates a file specified by -t" do before do @buffer = StringIO.new @origin_filename = "plain.env" @template_filename = "plain.env.template" end it "templates variables" do @input = StringIO.new("FOO=BAR\nFOO2=BAR2") allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input) allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer) # call the function that writes to the file Dotenv::CLI.new(["-t", @origin_filename]) # reading the buffer and checking its content. expect(@buffer.string).to eq("FOO=FOO\nFOO2=FOO2\n") end it "templates variables with export prefix" do @input = StringIO.new("export FOO=BAR\nexport FOO2=BAR2") allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input) allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer) Dotenv::CLI.new(["-t", @origin_filename]) expect(@buffer.string).to eq("export FOO=FOO\nexport FOO2=FOO2\n") end it "templates multi-line variables" do @input = StringIO.new(<<~TEXT) FOO=BAR FOO2="BAR2 BAR2" TEXT allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input) allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer) # call the function that writes to the file Dotenv::CLI.new(["-t", @origin_filename]) # reading the buffer and checking its content. expect(@buffer.string).to eq("FOO=FOO\nFOO2=FOO2\n") end it "ignores blank lines" do @input = StringIO.new("\nFOO=BAR\nFOO2=BAR2") allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input) allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer) Dotenv::CLI.new(["-t", @origin_filename]) expect(@buffer.string).to eq("\nFOO=FOO\nFOO2=FOO2\n") end it "ignores comments" do @comment_input = StringIO.new("#Heading comment\nFOO=BAR\nFOO2=BAR2\n") allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_input) allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer) Dotenv::CLI.new(["-t", @origin_filename]) expect(@buffer.string).to eq("#Heading comment\nFOO=FOO\nFOO2=FOO2\n") end it "ignores comments with =" do @comment_with_equal_input = StringIO.new("#Heading=comment\nFOO=BAR\nFOO2=BAR2") allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_with_equal_input) allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer) Dotenv::CLI.new(["-t", @origin_filename]) expect(@buffer.string).to eq("#Heading=comment\nFOO=FOO\nFOO2=FOO2\n") end it "ignores comments with leading spaces" do @comment_leading_spaces_input = StringIO.new(" #Heading comment\nFOO=BAR\nFOO2=BAR2") allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_leading_spaces_input) allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer) Dotenv::CLI.new(["-t", @origin_filename]) expect(@buffer.string).to eq(" #Heading comment\nFOO=FOO\nFOO2=FOO2\n") end end end ================================================ FILE: spec/dotenv/diff_spec.rb ================================================ require "spec_helper" describe Dotenv::Diff do let(:before) { {} } let(:after) { {} } subject { Dotenv::Diff.new(a: before, b: after) } context "no changes" do let(:before) { {"A" => 1} } let(:after) { {"A" => 1} } it { expect(subject.added).to eq({}) } it { expect(subject.removed).to eq({}) } it { expect(subject.changed).to eq({}) } it { expect(subject.any?).to eq(false) } it { expect(subject.env).to eq({}) } end context "key added" do let(:after) { {"A" => 1} } it { expect(subject.added).to eq("A" => 1) } it { expect(subject.removed).to eq({}) } it { expect(subject.changed).to eq({}) } it { expect(subject.any?).to eq(true) } it { expect(subject.env).to eq("A" => 1) } end context "key removed" do let(:before) { {"A" => 1} } it { expect(subject.added).to eq({}) } it { expect(subject.removed).to eq("A" => 1) } it { expect(subject.changed).to eq({}) } it { expect(subject.any?).to eq(true) } it { expect(subject.env).to eq("A" => nil) } end context "key changed" do let(:before) { {"A" => 1} } let(:after) { {"A" => 2} } it { expect(subject.added).to eq({}) } it { expect(subject.removed).to eq({}) } it { expect(subject.changed).to eq("A" => [1, 2]) } it { expect(subject.any?).to eq(true) } it { expect(subject.env).to eq("A" => 2) } end end ================================================ FILE: spec/dotenv/environment_spec.rb ================================================ require "spec_helper" describe Dotenv::Environment do subject { env("OPTION_A=1\nOPTION_B=2") } describe "initialize" do it "reads the file" do expect(subject["OPTION_A"]).to eq("1") expect(subject["OPTION_B"]).to eq("2") end it "fails if file does not exist" do expect do Dotenv::Environment.new(".does_not_exists") end.to raise_error(Errno::ENOENT) end end require "tempfile" def env(text, ...) file = Tempfile.new("dotenv") file.write text file.close env = Dotenv::Environment.new(file.path, ...) file.unlink env end end ================================================ FILE: spec/dotenv/log_subscriber_spec.rb ================================================ require "spec_helper" require "active_support/all" require "rails" require "dotenv/rails" describe Dotenv::LogSubscriber do let(:logs) { StringIO.new } before do Dotenv.instrumenter = ActiveSupport::Notifications Dotenv::Rails.logger = Logger.new(logs) end describe "load" do it "logs when a file is loaded" do Dotenv.load(fixture_path("plain.env")) expect(logs.string).to match(/Loaded.*plain.env/) expect(logs.string).to match(/Set.*PLAIN/) end end context "update" do it "logs when a new instance variable is set" do Dotenv.update({"PLAIN" => "true"}) expect(logs.string).to match(/Set.*PLAIN/) end it "logs when an instance variable is overwritten" do ENV["PLAIN"] = "nope" Dotenv.update({"PLAIN" => "true"}, overwrite: true) expect(logs.string).to match(/Set.*PLAIN/) end it "does not log when an instance variable is not overwritten" do ENV["FOO"] = "existing" Dotenv.update({"FOO" => "new"}) expect(logs.string).not_to match(/FOO/) end it "does not log when an instance variable is unchanged" do ENV["PLAIN"] = "true" Dotenv.update({"PLAIN" => "true"}, overwrite: true) expect(logs.string).not_to match(/PLAIN/) end end context "save" do it "logs when a snapshot is saved" do Dotenv.save expect(logs.string).to match(/Saved/) end end context "restore" do it "logs restored keys" do previous_value = ENV["PWD"] ENV["PWD"] = "/tmp" Dotenv.restore expect(logs.string).to match(/Restored.*PWD/) # Does not log value expect(logs.string).not_to include(previous_value) end it "logs unset keys" do ENV["DOTENV_TEST"] = "LogSubscriber" Dotenv.restore expect(logs.string).to match(/Unset.*DOTENV_TEST/) end it "does not log if no keys unset or restored" do Dotenv.restore expect(logs.string).not_to match(/Restored|Unset/) end end end ================================================ FILE: spec/dotenv/parser_spec.rb ================================================ require "spec_helper" describe Dotenv::Parser do def env(...) Dotenv::Parser.call(...) end it "parses unquoted values" do expect(env("FOO=bar")).to eql("FOO" => "bar") end it "parses unquoted values with spaces after seperator" do expect(env("FOO= bar")).to eql("FOO" => "bar") end it "parses unquoted escape characters correctly" do expect(env("FOO=bar\\ bar")).to eql("FOO" => "bar bar") end it "parses values with spaces around equal sign" do expect(env("FOO =bar")).to eql("FOO" => "bar") expect(env("FOO= bar")).to eql("FOO" => "bar") end it "parses values with leading spaces" do expect(env(" FOO=bar")).to eql("FOO" => "bar") end it "parses values with following spaces" do expect(env("FOO=bar ")).to eql("FOO" => "bar") end it "parses double quoted values" do expect(env('FOO="bar"')).to eql("FOO" => "bar") end it "parses double quoted values with following spaces" do expect(env('FOO="bar" ')).to eql("FOO" => "bar") end it "parses single quoted values" do expect(env("FOO='bar'")).to eql("FOO" => "bar") end it "parses single quoted values with following spaces" do expect(env("FOO='bar' ")).to eql("FOO" => "bar") end it "parses escaped double quotes" do expect(env('FOO="escaped\"bar"')).to eql("FOO" => 'escaped"bar') end it "parses empty values" do expect(env("FOO=")).to eql("FOO" => "") end it "expands variables found in values" do expect(env("FOO=test\nBAR=$FOO")).to eql("FOO" => "test", "BAR" => "test") end it "parses variables wrapped in brackets" do expect(env("FOO=test\nBAR=${FOO}bar")) .to eql("FOO" => "test", "BAR" => "testbar") end it "expands variables from ENV if not found in .env" do ENV["FOO"] = "test" expect(env("BAR=$FOO")).to eql("BAR" => "test") end it "expands variables from ENV if found in .env during load" do ENV["FOO"] = "test" expect(env("FOO=development\nBAR=${FOO}")["BAR"]) .to eql("test") end it "doesn't expand variables from ENV if in local env in overwrite" do ENV["FOO"] = "test" expect(env("FOO=development\nBAR=${FOO}")["BAR"]) .to eql("test") end it "expands undefined variables to an empty string" do expect(env("BAR=$FOO")).to eql("BAR" => "") end it "expands variables in double quoted strings" do expect(env("FOO=test\nBAR=\"quote $FOO\"")) .to eql("FOO" => "test", "BAR" => "quote test") end it "does not expand variables in single quoted strings" do expect(env("BAR='quote $FOO'")).to eql("BAR" => "quote $FOO") end it "does not expand escaped variables" do expect(env('FOO="foo\$BAR"')).to eql("FOO" => "foo$BAR") expect(env('FOO="foo\${BAR}"')).to eql("FOO" => "foo${BAR}") expect(env("FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"")) .to eql("FOO" => "test", "BAR" => "foo${FOO} test") end it "parses yaml style options" do expect(env("OPTION_A: 1")).to eql("OPTION_A" => "1") end it "parses export keyword" do expect(env("export OPTION_A=2")).to eql("OPTION_A" => "2") end it "allows export line if you want to do it that way" do expect(env('OPTION_A=2 export OPTION_A')).to eql("OPTION_A" => "2") end it "allows export line if you want to do it that way and checks for unset variables" do expect do env('OPTION_A=2 export OH_NO_NOT_SET') end.to raise_error(Dotenv::FormatError, 'Line "export OH_NO_NOT_SET" has an unset variable') end it 'escapes \n in quoted strings' do expect(env('FOO="bar\nbaz"')).to eql("FOO" => "bar\\nbaz") expect(env('FOO="bar\\nbaz"')).to eql("FOO" => "bar\\nbaz") end it 'expands \n and \r in quoted strings with DOTENV_LINEBREAK_MODE=legacy in current file' do ENV["DOTENV_LINEBREAK_MODE"] = "strict" contents = [ "DOTENV_LINEBREAK_MODE=legacy", 'FOO="bar\nbaz\rfizz"' ].join("\n") expect(env(contents)).to eql("DOTENV_LINEBREAK_MODE" => "legacy", "FOO" => "bar\nbaz\rfizz") end it 'expands \n and \r in quoted strings with DOTENV_LINEBREAK_MODE=legacy in ENV' do ENV["DOTENV_LINEBREAK_MODE"] = "legacy" contents = 'FOO="bar\nbaz\rfizz"' expect(env(contents)).to eql("FOO" => "bar\nbaz\rfizz") end it 'parses variables with "." in the name' do expect(env("FOO.BAR=foobar")).to eql("FOO.BAR" => "foobar") end it "strips unquoted values" do expect(env("foo=bar ")).to eql("foo" => "bar") # not 'bar ' end it "ignores lines that are not variable assignments" do expect(env("lol$wut")).to eql({}) end it "ignores empty lines" do expect(env("\n \t \nfoo=bar\n \nfizz=buzz")) .to eql("foo" => "bar", "fizz" => "buzz") end it "does not ignore empty lines in quoted string" do value = "a\n\nb\n\nc" expect(env("FOO=\"#{value}\"")).to eql("FOO" => value) end it "ignores inline comments" do expect(env("foo=bar # this is foo")).to eql("foo" => "bar") end it "allows # in quoted value" do expect(env('foo="bar#baz" # comment')).to eql("foo" => "bar#baz") end it "allows # in quoted value with spaces after seperator" do expect(env('foo= "bar#baz" # comment')).to eql("foo" => "bar#baz") end it "ignores comment lines" do expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql("foo" => "bar") end it "ignores commented out variables" do expect(env("# HELLO=world\n")).to eql({}) end it "ignores comment" do expect(env("# Uncomment to activate:\n")).to eql({}) end it "includes variables without values" do input = 'DATABASE_PASSWORD= DATABASE_USERNAME=root DATABASE_HOST=/tmp/mysql.sock' output = { "DATABASE_PASSWORD" => "", "DATABASE_USERNAME" => "root", "DATABASE_HOST" => "/tmp/mysql.sock" } expect(env(input)).to eql(output) end it "parses # in quoted values" do expect(env('foo="ba#r"')).to eql("foo" => "ba#r") expect(env("foo='ba#r'")).to eql("foo" => "ba#r") end it "parses # in quoted values with following spaces" do expect(env('foo="ba#r" ')).to eql("foo" => "ba#r") expect(env("foo='ba#r' ")).to eql("foo" => "ba#r") end it "parses empty values" do expect(env("foo=")).to eql("foo" => "") end it "allows multi-line values in single quotes" do env_file = %(OPTION_A=first line export OPTION_B='line 1 line 2 line 3' OPTION_C="last line" OPTION_ESCAPED='line one this is \\'quoted\\' one more line') expected_result = { "OPTION_A" => "first line", "OPTION_B" => "line 1\nline 2\nline 3", "OPTION_C" => "last line", "OPTION_ESCAPED" => "line one\nthis is \\'quoted\\'\none more line" } expect(env(env_file)).to eql(expected_result) end it "allows multi-line values in double quotes" do env_file = %(OPTION_A=first line export OPTION_B="line 1 line 2 line 3" OPTION_C="last line" OPTION_ESCAPED="line one this is \\"quoted\\" one more line") expected_result = { "OPTION_A" => "first line", "OPTION_B" => "line 1\nline 2\nline 3", "OPTION_C" => "last line", "OPTION_ESCAPED" => "line one\nthis is \"quoted\"\none more line" } expect(env(env_file)).to eql(expected_result) end if RUBY_VERSION > "1.8.7" it "parses shell commands interpolated in $()" do expect(env("echo=$(echo hello)")).to eql("echo" => "hello") end it "allows balanced parentheses within interpolated shell commands" do expect(env('echo=$(echo "$(echo "$(echo "$(echo hello)")")")')) .to eql("echo" => "hello") end it "doesn't interpolate shell commands when escape says not to" do expect(env('echo=escaped-\$(echo hello)')) .to eql("echo" => "escaped-$(echo hello)") end it "is not thrown off by quotes in interpolated shell commands" do expect(env('interp=$(echo "Quotes won\'t be a problem")')["interp"]) .to eql("Quotes won't be a problem") end it "handles parentheses in variables in commands" do expect(env("FOO='passwo(rd'\nBAR=$(echo '$FOO')")).to eql("FOO" => "passwo(rd", "BAR" => "passwo(rd") end it "handles command to variable to command chain" do expect(env("FOO=$(echo bar)\nBAR=$(echo $FOO)")).to eql("FOO" => "bar", "BAR" => "bar") end it "supports carriage return" do expect(env("FOO=bar\rbaz=fbb")).to eql("FOO" => "bar", "baz" => "fbb") end it "supports carriage return combine with new line" do expect(env("FOO=bar\r\nbaz=fbb")).to eql("FOO" => "bar", "baz" => "fbb") end it "escapes carriage return in quoted strings" do expect(env('FOO="bar\rbaz"')).to eql("FOO" => "bar\\rbaz") end it "escape $ properly when no alphabets/numbers/_ are followed by it" do expect(env("FOO=\"bar\\$ \\$\\$\"")).to eql("FOO" => "bar$ $$") end # echo bar $ -> prints bar $ in the shell it "ignore $ when it is not escaped and no variable is followed by it" do expect(env("FOO=\"bar $ \"")).to eql("FOO" => "bar $ ") end # This functionality is not supported on JRuby or Rubinius if (!defined?(RUBY_ENGINE) || RUBY_ENGINE != "jruby") && !defined?(Rubinius) it "substitutes shell variables within interpolated shell commands" do expect(env(%(VAR1=var1\ninterp=$(echo "VAR1 is $VAR1")))["interp"]) .to eql("VAR1 is var1") end end end it "returns existing value for redefined variable" do ENV["FOO"] = "existing" expect(env("FOO=bar")).to eql("FOO" => "existing") end end ================================================ FILE: spec/dotenv/rails_spec.rb ================================================ require "spec_helper" require "rails" require "dotenv/rails" describe Dotenv::Rails do let(:log_output) { StringIO.new } let(:application) do log_output = self.log_output Class.new(Rails::Application) do config.load_defaults Rails::VERSION::STRING.to_f config.eager_load = false config.logger = ActiveSupport::Logger.new(log_output) config.root = fixture_path # Remove method fails since app is reloaded for each test config.active_support.remove_deprecated_time_with_zone_name = false end.instance end around do |example| # These get frozen after the app initializes autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup # Run in fixtures directory Dir.chdir(fixture_path) { example.run } ensure # Restore autoload paths to unfrozen state ActiveSupport::Dependencies.autoload_paths = autoload_paths ActiveSupport::Dependencies.autoload_once_paths = autoload_once_paths end before do Rails.env = "test" Rails.application = nil Rails.logger = nil begin # Remove the singleton instance if it exists Dotenv::Rails.remove_instance_variable(:@instance) rescue nil end end describe "files" do it "loads files for development environment" do Rails.env = "development" expect(Dotenv::Rails.files).to eql( [ ".env.development.local", ".env.local", ".env.development", ".env" ] ) end it "does not load .env.local in test rails environment" do Rails.env = "test" expect(Dotenv::Rails.files).to eql( [ ".env.test.local", ".env.test", ".env" ] ) end it "can be modified in place" do Dotenv::Rails.files << ".env.shared" expect(Dotenv::Rails.files.last).to eq(".env.shared") end end it "watches other loaded files with Spring" do stub_spring(load_watcher: true) application.initialize! path = fixture_path("plain.env") Dotenv.load(path) expect(Spring.watcher).to include(path.to_s) end it "doesn't raise an error if Spring.watch is not defined" do stub_spring(load_watcher: false) expect { application.initialize! }.to_not raise_error end context "before_configuration" do it "calls #load" do expect(Dotenv::Rails.instance).to receive(:load) ActiveSupport.run_load_hooks(:before_configuration) end end context "load" do subject { application.initialize! } it "watches .env with Spring" do stub_spring(load_watcher: true) subject expect(Spring.watcher).to include(fixture_path(".env").to_s) end it "loads .env.test before .env" do subject expect(ENV["DOTENV"]).to eql("test") end it "loads configured files" do Dotenv::Rails.files = [fixture_path("plain.env")] expect { subject }.to change { ENV["PLAIN"] }.from(nil).to("true") end it "loads file relative to Rails.root" do allow(Rails).to receive(:root).and_return(Pathname.new("/tmp")) Dotenv::Rails.files = [".env"] expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false}) subject end it "returns absolute paths unchanged" do Dotenv::Rails.files = ["/tmp/.env"] expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false}) subject end context "with overwrite = true" do before { Dotenv::Rails.overwrite = true } it "overwrites .env with .env.test" do subject expect(ENV["DOTENV"]).to eql("test") end it "overwrites any existing ENV variables" do ENV["DOTENV"] = "predefined" expect { subject }.to(change { ENV["DOTENV"] }.from("predefined").to("test")) end end end describe "root" do it "returns Rails.root" do expect(Dotenv::Rails.root).to eql(Rails.root) end context "when Rails.root is nil" do before do allow(Rails).to receive(:root).and_return(nil) end it "falls back to RAILS_ROOT" do ENV["RAILS_ROOT"] = "/tmp" expect(Dotenv::Rails.root.to_s).to eql("/tmp") end end end describe "autorestore" do it "is loaded if RAILS_ENV=test" do expect(Dotenv::Rails.autorestore).to eq(true) expect(Dotenv::Rails.instance).to receive(:require).with("dotenv/autorestore") application.initialize! end it "is not loaded if RAILS_ENV=development" do Rails.env = "development" expect(Dotenv::Rails.autorestore).to eq(false) expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore") application.initialize! end it "is not loaded if autorestore set to false" do Dotenv::Rails.autorestore = false expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore") application.initialize! end it "is not loaded if ClimateControl is defined" do stub_const("ClimateControl", Module.new) expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore") application.initialize! end it "is not loaded if IceAge is defined" do stub_const("IceAge", Module.new) expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore") application.initialize! end end describe "logger" do it "replays to Rails.logger" do expect(Dotenv::Rails.logger).to be_a(Dotenv::ReplayLogger) Dotenv::Rails.logger.debug("test") application.initialize! expect(Dotenv::Rails.logger).not_to be_a(Dotenv::ReplayLogger) expect(log_output.string).to include("[dotenv] test") end it "does not replace custom logger" do logger = Logger.new(log_output) Dotenv::Rails.logger = logger application.initialize! expect(Dotenv::Rails.logger).to be(logger) end end def stub_spring(load_watcher: true) spring = Module.new do if load_watcher def self.watcher @watcher ||= Set.new end def self.watch(path) watcher.add path end end end stub_const "Spring", spring end end ================================================ FILE: spec/dotenv_spec.rb ================================================ require "spec_helper" describe Dotenv do before do Dir.chdir(File.expand_path("../fixtures", __FILE__)) end shared_examples "load" do context "with no args" do let(:env_files) { [] } it "defaults to .env" do expect(Dotenv::Environment).to receive(:new).with(expand(".env"), anything).and_call_original subject end end context "with a tilde path" do let(:env_files) { ["~/.env"] } it "expands the path" do expected = expand("~/.env") allow(File).to receive(:exist?) { |arg| arg == expected } expect(Dotenv::Environment).to receive(:new).with(expected, anything) .and_return(Dotenv::Environment.new(".env")) subject end end context "with multiple files" do let(:env_files) { [".env", fixture_path("plain.env")] } let(:expected) do {"OPTION_A" => "1", "OPTION_B" => "2", "OPTION_C" => "3", "OPTION_D" => "4", "OPTION_E" => "5", "PLAIN" => "true", "DOTENV" => "true"} end it "loads all files" do subject expected.each do |key, value| expect(ENV[key]).to eq(value) end end it "returns hash of loaded variables" do expect(subject).to eq(expected) end it "does not return unchanged variables" do ENV["OPTION_A"] = "1" expect(subject).not_to have_key("OPTION_A") end end end shared_examples "overwrite" do it_behaves_like "load" context "with multiple files" do let(:env_files) { [fixture_path("important.env"), fixture_path("plain.env")] } let(:expected) do { "OPTION_A" => "abc", "OPTION_B" => "2", "OPTION_C" => "3", "OPTION_D" => "4", "OPTION_E" => "5", "PLAIN" => "false" } end it "respects the file importance order" do subject expected.each do |key, value| expect(ENV[key]).to eq(value) end end end end describe "load" do let(:env_files) { [] } let(:options) { {} } subject { Dotenv.load(*env_files, **options) } it_behaves_like "load" it "initializes the Environment with overwrite: false" do expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false) .and_call_original subject end it "warns about not overwriting when requested" do options[:overwrite] = :warn ENV["DOTENV"] = "false" expect(capture_output { subject }).to eq("Warning: dotenv not overwriting ENV[\"DOTENV\"]\n") expect(ENV["DOTENV"]).to eq("false") end context "when the file does not exist" do let(:env_files) { [".env_does_not_exist"] } it "fails silently" do expect { subject }.not_to raise_error end it "does not change ENV" do expect { subject }.not_to change { ENV.inspect } end end context "when the file is a directory" do let(:env_files) { [] } around do |example| Dir.mktmpdir do |dir| env_files.push dir example.run end end it "fails silently with ignore: true (default)" do expect { subject }.not_to raise_error end it "raises error with ignore: false" do options[:ignore] = false expect { subject }.to raise_error(/Is a directory/) end end end describe "load!" do let(:env_files) { [] } subject { Dotenv.load!(*env_files) } it_behaves_like "load" it "initializes Environment with overwrite: false" do expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false) .and_call_original subject end context "when one file exists and one does not" do let(:env_files) { [".env", ".env_does_not_exist"] } it "raises an Errno::ENOENT error" do expect { subject }.to raise_error(Errno::ENOENT) end end end describe "overwrite" do let(:env_files) { [fixture_path("plain.env")] } subject { Dotenv.overwrite(*env_files) } it_behaves_like "load" it_behaves_like "overwrite" it "initializes the Environment overwrite: true" do expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true) .and_call_original subject end context "when loading a file containing already set variables" do let(:env_files) { [fixture_path("plain.env")] } it "overwrites any existing ENV variables" do ENV["OPTION_A"] = "predefined" subject expect(ENV["OPTION_A"]).to eq("1") end end context "when the file does not exist" do let(:env_files) { [".env_does_not_exist"] } it "fails silently" do expect { subject }.not_to raise_error end it "does not change ENV" do expect { subject }.not_to change { ENV.inspect } end end end describe "overwrite!" do let(:env_files) { [fixture_path("plain.env")] } subject { Dotenv.overwrite!(*env_files) } it_behaves_like "load" it_behaves_like "overwrite" it "initializes the Environment with overwrite: true" do expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true) .and_call_original subject end context "when loading a file containing already set variables" do let(:env_files) { [fixture_path("plain.env")] } it "overwrites any existing ENV variables" do ENV["OPTION_A"] = "predefined" subject expect(ENV["OPTION_A"]).to eq("1") end end context "when one file exists and one does not" do let(:env_files) { [".env", ".env_does_not_exist"] } it "raises an Errno::ENOENT error" do expect { subject }.to raise_error(Errno::ENOENT) end end end describe "with an instrumenter" do let(:instrumenter) { double("instrumenter", instrument: {}) } before { Dotenv.instrumenter = instrumenter } after { Dotenv.instrumenter = nil } describe "load" do it "instruments if the file exists" do expect(instrumenter).to receive(:instrument) do |name, payload| expect(name).to eq("load.dotenv") expect(payload[:env]).to be_instance_of(Dotenv::Environment) {} end Dotenv.load end it "does not instrument if file does not exist" do expect(instrumenter).to receive(:instrument).never Dotenv.load ".doesnotexist" end end end describe "require keys" do let(:env_files) { [".env", fixture_path("bom.env")] } before { Dotenv.load(*env_files) } it "raises exception with not defined mandatory ENV keys" do expect { Dotenv.require_keys("BOM", "TEST") }.to raise_error( Dotenv::MissingKeys, 'Missing required configuration key: ["TEST"]' ) end it "raises exception when missing multiple mandator keys" do expect { Dotenv.require_keys("TEST1", "TEST2") }.to raise_error( Dotenv::MissingKeys, 'Missing required configuration keys: ["TEST1", "TEST2"]' ) end end describe "parse" do let(:env_files) { [] } subject { Dotenv.parse(*env_files) } context "with no args" do let(:env_files) { [] } it "defaults to .env" do expect(Dotenv::Environment).to receive(:new).with(expand(".env"), anything) subject end end context "with a tilde path" do let(:env_files) { ["~/.env"] } it "expands the path" do expected = expand("~/.env") allow(File).to receive(:exist?) { |arg| arg == expected } expect(Dotenv::Environment).to receive(:new).with(expected, anything) subject end end context "with multiple files" do let(:env_files) { [".env", fixture_path("plain.env")] } let(:expected) do {"OPTION_A" => "1", "OPTION_B" => "2", "OPTION_C" => "3", "OPTION_D" => "4", "OPTION_E" => "5", "PLAIN" => "true", "DOTENV" => "true"} end it "does not modify ENV" do subject expected.each do |key, _value| expect(ENV[key]).to be_nil end end it "returns hash of parsed key/value pairs" do expect(subject).to eq(expected) end end it "initializes the Environment with overwrite: false" do expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false) subject end context "when the file does not exist" do let(:env_files) { [".env_does_not_exist"] } it "fails silently" do expect { subject }.not_to raise_error expect(subject).to eq({}) end end end describe "Unicode" do subject { fixture_path("bom.env") } it "loads a file with a Unicode BOM" do expect(Dotenv.load(subject)).to eql("BOM" => "UTF-8") end it "fixture file has UTF-8 BOM" do contents = File.binread(subject).force_encoding("UTF-8") expect(contents).to start_with("\xEF\xBB\xBF".force_encoding("UTF-8")) end end describe "restore" do it "restores previously saved snapshot" do ENV["MODIFIED"] = "true" Dotenv.restore # save was already called in setup expect(ENV["MODIFIED"]).to be_nil end it "raises an error in threads" do ENV["MODIFIED"] = "true" Thread.new do expect { Dotenv.restore }.to raise_error(ThreadError, /not thread safe/) end.join expect(ENV["MODIFIED"]).to eq("true") end it "is a noop if nil state provided" do expect { Dotenv.restore(nil) }.not_to raise_error end it "is a noop if no previously saved state" do # Clear state saved in setup expect(Dotenv.instance_variable_get(:@diff)).to be_instance_of(Dotenv::Diff) Dotenv.instance_variable_set(:@diff, nil) expect { Dotenv.restore }.not_to raise_error end it "can save and restore stubbed ENV" do stub_const("ENV", ENV.to_h.merge("STUBBED" => "1")) Dotenv.save ENV["MODIFIED"] = "1" Dotenv.restore expect(ENV["STUBBED"]).to eq("1") expect(ENV["MODIFED"]).to be(nil) end end describe "modify" do it "sets values for the block" do ENV["FOO"] = "initial" Dotenv.modify(FOO: "during", BAR: "baz") do expect(ENV["FOO"]).to eq("during") expect(ENV["BAR"]).to eq("baz") end expect(ENV["FOO"]).to eq("initial") expect(ENV).not_to have_key("BAR") end end describe "update" do it "sets new variables" do Dotenv.update({"OPTION_A" => "1"}) expect(ENV["OPTION_A"]).to eq("1") end it "does not overwrite defined variables" do ENV["OPTION_A"] = "original" Dotenv.update({"OPTION_A" => "updated"}) expect(ENV["OPTION_A"]).to eq("original") end context "with overwrite: true" do it "sets new variables" do Dotenv.update({"OPTION_A" => "1"}, overwrite: true) expect(ENV["OPTION_A"]).to eq("1") end it "overwrites defined variables" do ENV["OPTION_A"] = "original" Dotenv.update({"OPTION_A" => "updated"}, overwrite: true) expect(ENV["OPTION_A"]).to eq("updated") end end end def expand(path) File.expand_path path end end ================================================ FILE: spec/fixtures/bom.env ================================================ BOM=UTF-8 ================================================ FILE: spec/fixtures/exported.env ================================================ export OPTION_A=2 export OPTION_B='\n' ================================================ FILE: spec/fixtures/important.env ================================================ PLAIN=false OPTION_A=abc OPTION_B=2 ================================================ FILE: spec/fixtures/plain.env ================================================ PLAIN=true OPTION_A=1 OPTION_B=2 OPTION_C= 3 OPTION_D =4 OPTION_E = 5 ================================================ FILE: spec/fixtures/quoted.env ================================================ QUOTED=true OPTION_A='1' OPTION_B='2' OPTION_C='' OPTION_D='\n' OPTION_E="1" OPTION_F="2" OPTION_G="" OPTION_H="\n" ================================================ FILE: spec/fixtures/yaml.env ================================================ OPTION_A: 1 OPTION_B: '2' OPTION_C: '' OPTION_D: '\n' ================================================ FILE: spec/spec_helper.rb ================================================ require "dotenv" require "dotenv/autorestore" require "tmpdir" def fixture_path(*parts) Pathname.new(__dir__).join("./fixtures", *parts) end # Capture output to $stdout and $stderr def capture_output(&_block) original_stderr = $stderr original_stdout = $stdout output = $stderr = $stdout = StringIO.new yield output.string ensure $stderr = original_stderr $stdout = original_stdout end ================================================ FILE: test/autorestore_test.rb ================================================ require "active_support" # Rails 6.1 fails if this is not loaded require "active_support/test_case" require "minitest/autorun" require "dotenv" require "dotenv/autorestore" class AutorestoreTest < ActiveSupport::TestCase test "restores ENV between tests, part 1" do assert_nil ENV["DOTENV"], "ENV was not restored between tests" ENV["DOTENV"] = "1" end test "restores ENV between tests, part 2" do assert_nil ENV["DOTENV"], "ENV was not restored between tests" ENV["DOTENV"] = "2" end end