New Upstream Release - ruby-dotenv

Ready changes

Summary

Merged new upstream version: 2.8.1 (was: 2.4.0).

Resulting package

Built on 2023-01-16T10:39 (took 4m36s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases ruby-dotenv

Diff

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..403b308
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+  - package-ecosystem: "bundler"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3d661b6
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: CI
+on:
+  push:
+    branches: [master]
+  pull_request:
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        ruby: ['2.5', '2.6', '2.7', '3.0', '3.1', '3.2']
+    steps:
+      - name: Check out repository code
+        uses: actions/checkout@v3
+      - name: Set up Ruby
+        uses: ruby/setup-ruby@v1
+        with:
+          bundler-cache: true
+          ruby-version: ${{ matrix.ruby }}
+      - name: Run Rake
+        run: bundle exec rake
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d8b2ec0
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,19 @@
+name: Publish Gem
+on: release
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Release dotenv
+        uses: cadwallion/publish-rubygems-action@master
+        env:
+          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+          RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
+          RELEASE_COMMAND: rake dotenv:release
+      - name: Release dotenv-rails
+        uses: cadwallion/publish-rubygems-action@master
+        env:
+          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+          RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
+          RELEASE_COMMAND: rake dotenv-rails:release
diff --git a/.rubocop.yml b/.rubocop.yml
deleted file mode 100644
index e98412d..0000000
--- a/.rubocop.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-Lint/HandleExceptions:
-  Exclude:
-    - 'lib/dotenv/rails.rb'
-    - 'lib/dotenv.rb'
-
-Style/Documentation:
-  Exclude:
-    - 'Rakefile'
-    - 'lib/dotenv/version.rb'
-
-Style/FileName:
-  Exclude:
-    - 'lib/dotenv/rails-now.rb'
-    - 'lib/dotenv-rails.rb'
-
-Style/HashSyntax:
-  EnforcedStyle: 'ruby19'
-
-Style/StringLiterals:
-  EnforcedStyle: 'double_quotes'
diff --git a/.standard.yml b/.standard.yml
new file mode 100644
index 0000000..7a9789d
--- /dev/null
+++ b/.standard.yml
@@ -0,0 +1,5 @@
+ruby_version: 2.5
+
+ignore:
+  - lib/dotenv/parser.rb:
+      - Lint/InheritException
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 489ea26..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-language: ruby
-
-rvm:
-  - 2.5.0
-  - 2.4.1
-  - 2.3.4
-  - 2.2.7
-  - 2.1.10
-  - 2.0.0
-
-before_install:
-  - gem update --remote bundler
-  - gem update --system
-
-sudo: false
-
-bundler_args: --without=guard
-
-notifications:
-  disabled: true
-
-script:
-  - bundle exec rake
diff --git a/Changelog.md b/Changelog.md
index 28f8d9e..d9e371b 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,6 +1,69 @@
 # Changelog
 
-[Unreleased changes](https://github.com/bkeepers/dotenv/compare/v2.4.0...master)
+[Unreleased changes](https://github.com/bkeepers/dotenv/compare/v2.8.1...master)
+
+## 2.8.1 - July 27, 2022
+
+* Restore support for EOL'd Ruby versions (2.5, 2.6) (#458)[https://github.com/bkeepers/dotenv/pull/458]
+
+## 2.8.0 - July 26, 2022
+
+* Drop support for Ruby 2.4, 2.5, and 2.6, which are EOL
+* Fix template (-t) handling of export declarations [#416](https://github.com/bkeepers/dotenv/pull/416)
+* Unescape escaped characters when value is not quoted [#421](https://github.com/bkeepers/dotenv/pull/421)
+* Add overload option for the CLI (`$ dotenv --overload -f .env.local`) [#445](https://github.com/bkeepers/dotenv/pull/445)
+
+## 2.7.6 - July 11, 2020
+
+* Add a Dotenv::Railtie.overload method [#403](https://github.com/bkeepers/dotenv/pull/403)
+* Support for more Rails versions [#404](https://github.com/bkeepers/dotenv/pull/404)
+* Fix template handling of blank lines and comments [#413](https://github.com/bkeepers/dotenv/pull/413)
+* Fix for dotenv-rails Rake task environment allocation[#405](https://github.com/bkeepers/dotenv/pull/405)
+
+## 2.7.5 - July 31st, 2019
+
+* Fix for \s after separator [#399](https://github.com/bkeepers/dotenv/pull/399)
+* README formatting updates [#398](https://github.com/bkeepers/dotenv/pull/398)
+
+## 2.7.4 - June 23rd, 2019
+
+* Fix `NoMethodError` in non-Rails environments [#394](https://github.com/bkeepers/dotenv/pull/394)
+
+## 2.7.3 - June 22nd, 2019
+
+* Fix for parallel spec tasks initializing in development [#384](https://github.com/bkeepers/dotenv/pull/384)
+* Test against updated rubies [#383](https://github.com/bkeepers/dotenv/pull/383), [#387](https://github.com/bkeepers/dotenv/pull/387)
+* Conditional branch cleanup for clarity of intent [#385](https://github.com/bkeepers/dotenv/pull/385)
+* Fix for load order issue with Railties [#391](https://github.com/bkeepers/dotenv/pull/391)
+* NEW: dotenv-templates using the -t flag [#377](https://github.com/bkeepers/dotenv/pull/377), [#393](https://github.com/bkeepers/dotenv/pull/393)
+
+
+## 2.7.2 - March 25th, 2019
+
+* Cleaned up CLI while resolving regressions in 2.7.1 [#382](https://github.com/bkeepers/dotenv/pull/382)
+
+## 2.7.1 - February 24, 2019
+
+* Fixes regression with CLI experience ([#376](https://github.com/bkeepers/dotenv/pull/376))
+
+## 2.7.0 - February 21, 2019
+
+* Add Dotenv.parse method ([#362](https://github.com/bkeepers/dotenv/pull/362))
+* Add Support for Rails 6.0 ([#370](https://github.com/bkeepers/dotenv/pull/370))
+* Improve dotenv CLI output ([#374](https://github.com/bkeepers/dotenv/pull/374))
+* Add GitHub Actions automation ([#369](https://github.com/bkeepers/dotenv/pull/369))
+* Test against Ruby 2.6 ([#372](https://github.com/bkeepers/dotenv/pull/372))
+
+## 2.6.0 - January 5, 2019
+
+* Added require keys method to raise if not defined ([#354](https://github.com/bkeepers/dotenv/pull/354))
+* Use latest Ruby version on CI ([#356](https://github.com/bkeepers/dotenv/pull/356), [#363](https://github.com/bkeepers/dotenv/pull/363))
+* Clarify variable hierarchy in README.md ([#358](https://github.com/bkeepers/dotenv/pull/358))
+* Use SVG Travis CI badge ([#360](https://github.com/bkeepers/dotenv/pull/360))
+
+## 2.5.0 - June 21, 2018
+
+This release re-introduces [multiline values](https://github.com/bkeepers/dotenv/pull/346) with proper protections against the regressions that were reported in 2.3.0, and corrects a [breaking change](https://github.com/bkeepers/dotenv/pull/345) in the API from 2.4 for `Dotenv::Environment`.
 
 ## 2.4.0 - Apr 23, 2018
 
diff --git a/Gemfile b/Gemfile
index 15323fe..f33d8ca 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,17 +7,3 @@ group :guard do
   gem "guard-bundler"
   gem "rb-fsevent"
 end
-
-platforms :rbx do
-  gem "rubysl", "~> 2.0" # if using anything in the ruby standard library
-end
-
-if Gem::Version.create(RUBY_VERSION) < Gem::Version.create("2.2.2")
-  # Rack 2 requires Ruby version >= 2.2.2
-  gem "rack", ">= 1.6.5", "< 2.0.0"
-end
-
-if Gem::Version.create(RUBY_VERSION) < Gem::Version.create("2.1.0")
-  # Nokogiri 1.7 requires Ruby version >= 2.1.0.
-  gem "nokogiri", ">= 1.6.8", "< 1.7.0"
-end
diff --git a/Guardfile b/Guardfile
index 4c6ae56..9b8bfbf 100644
--- a/Guardfile
+++ b/Guardfile
@@ -5,5 +5,5 @@ 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" }
+  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
 end
diff --git a/README.md b/README.md
index d350361..2691a02 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# dotenv [![Build Status](https://secure.travis-ci.org/bkeepers/dotenv.png?branch=master)](https://travis-ci.org/bkeepers/dotenv) [![Gem Version](https://badge.fury.io/rb/dotenv.svg)](https://badge.fury.io/rb/dotenv) [![Join the chat at https://gitter.im/bkeepers/dotenv](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bkeepers/dotenv?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+# dotenv [![Gem Version](https://badge.fury.io/rb/dotenv.svg)](https://badge.fury.io/rb/dotenv) [![Join the chat at https://gitter.im/bkeepers/dotenv](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bkeepers/dotenv?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
 Shim to load environment variables from `.env` into `ENV` in *development*.
 
@@ -30,7 +30,10 @@ dotenv is initialized in your Rails app during the `before_configuration` callba
 # config/application.rb
 Bundler.require(*Rails.groups)
 
-Dotenv::Railtie.load
+# Load dotenv only in development or test environment
+if ['development', 'test'].include? ENV['RAILS_ENV']
+  Dotenv::Railtie.load
+end
 
 HOSTNAME = ENV['HOSTNAME']
 ```
@@ -62,7 +65,7 @@ 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.
 
-```
+```ruby
 require 'dotenv'
 Dotenv.load('file1.env', 'file2.env')
 ```
@@ -116,9 +119,21 @@ export SECRET_KEY=YOURSECRETKEYGOESHERE
 If you need multiline variables, for example private keys, you can double quote strings and use the `\n` character for newlines:
 
 ```shell
-PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9…\n-----END DSA PRIVATE KEY-----\n"
+PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9...\n-----END DSA PRIVATE KEY-----\n"
+```
+
+Alternatively, multi-line values with line breaks are now supported for quoted values.
+
+```shell
+PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
+...
+HkVN9...
+...
+-----END DSA PRIVATE KEY-----"
 ```
 
+This is particularly helpful when using the Heroku command line plugin [`heroku-config`](https://github.com/xavdid/heroku-config) to pull configuration variables down that may have line breaks.
+
 ### Command Substitution
 
 You need to add the output of a command in one of your variables? Simply add it with `$(your_command)`:
@@ -151,6 +166,31 @@ SECRET_KEY=YOURSECRETKEYGOESHERE # comment
 SECRET_HASH="something-with-a-#-hash"
 ```
 
+### 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.
+
 ## Frequently Answered Questions
 
 ### Can I use dotenv in production?
@@ -163,23 +203,58 @@ If you use this gem to handle env vars for multiple Rails environments (developm
 
 ### What other .env* files can I use?
 
-`dotenv-rails` will load the following files, starting from the bottom. The first value set (or those already defined in the environment) take precedence:
+`dotenv-rails` will override in the following order (highest defined variable overrides lower):
+
+| Hierarchy Priority | Filename                 | Environment          | Should I `.gitignore`it?                            | Notes                                                        |
+| ------------------ | ------------------------ | -------------------- | --------------------------------------------------- | ------------------------------------------------------------ |
+| 1st (highest)      | `.env.development.local` | Development          | Yes!                                                | Local overrides of environment-specific settings.            |
+| 1st                | `.env.test.local`        | Test                 | Yes!                                                | Local overrides of environment-specific settings.            |
+| 1st                | `.env.production.local`  | Production           | Yes!                                                | Local overrides of environment-specific settings.            |
+| 2nd                | `.env.local`             | Wherever the file is | Definitely.                                         | Local overrides. This file is loaded for all environments _except_ `test`. |
+| 3rd                | `.env.development`       | Development          | No.                                                 | Shared environment-specific settings                         |
+| 3rd                | `.env.test`              | Test                 | No.                                                 | Shared environment-specific settings                         |
+| 3rd                | `.env.production`        | Production           | No.                                                 | Shared environment-specific settings                         |
+| Last               | `.env`                   | All Environments     | Depends (See [below](#should-i-commit-my-env-file)) | The Original®                                                |
 
-- `.env` - The Original®
-- `.env.development`, `.env.test`, `.env.production` - Environment-specific settings.
-- `.env.local` - Local overrides. This file is loaded for all environments _except_ `test`.
-- `.env.development.local`, `.env.test.local`, `.env.production.local` - Local overrides of environment-specific settings.
 
 ### 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.
 
+
+You can use the `-t` or `--template` flag on the dotenv cli to create a template of your `.env` file.
+```shell
+$ dotenv -t .env
+```
+A template will be created in your working directory named `{FINAME}.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
+```
+
 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 overriding 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.overload`.
 
+You can also use the `-o` or `--overload` flag on the dotenv cli to override existing `ENV` variables.
+```shell
+$ 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).
diff --git a/Rakefile b/Rakefile
index b434c34..7ceb0fd 100644
--- a/Rakefile
+++ b/Rakefile
@@ -6,13 +6,17 @@ namespace "dotenv" do
   Bundler::GemHelper.install_tasks name: "dotenv"
 end
 
-namespace "dotenv-rails" do
-  class DotenvRailsGemHelper < Bundler::GemHelper
-    def guard_already_tagged; end # noop
+class DotenvRailsGemHelper < Bundler::GemHelper
+  def guard_already_tagged
+    # noop
+  end
 
-    def tag_version; end # noop
+  def tag_version
+    # noop
   end
+end
 
+namespace "dotenv-rails" do
   DotenvRailsGemHelper.install_tasks name: "dotenv-rails"
 end
 
@@ -24,11 +28,10 @@ require "rspec/core/rake_task"
 
 desc "Run all specs"
 RSpec::Core::RakeTask.new(:spec) do |t|
-  t.rspec_opts = %w(--color)
+  t.rspec_opts = %w[--color]
   t.verbose = false
 end
 
-require "rubocop/rake_task"
-RuboCop::RakeTask.new
+require "standard/rake"
 
-task default: [:spec, :rubocop]
+task default: [:spec, :standard]
diff --git a/debian/changelog b/debian/changelog
index 8ed0ecf..0088bc5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,9 @@
-ruby-dotenv (2.4.0-3) UNRELEASED; urgency=medium
+ruby-dotenv (2.8.1-1) UNRELEASED; urgency=medium
 
   * Update standards version to 4.6.1, no changes needed.
+  * New upstream release.
 
- -- Debian Janitor <janitor@jelmer.uk>  Fri, 14 Oct 2022 06:52:33 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 16 Jan 2023 10:35:28 -0000
 
 ruby-dotenv (2.4.0-2) unstable; urgency=medium
 
diff --git a/dotenv-rails.gemspec b/dotenv-rails.gemspec
index 2688742..9cb4be4 100644
--- a/dotenv-rails.gemspec
+++ b/dotenv-rails.gemspec
@@ -2,17 +2,17 @@ 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 rails`.split(
+  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 rails`.split(
     $OUTPUT_RECORD_SEPARATOR
   ) + ["README.md", "LICENSE"]
 
   gem.add_dependency "dotenv", Dotenv::VERSION
-  gem.add_dependency "railties", ">= 3.2", "< 6.0"
+  gem.add_dependency "railties", ">= 3.2"
 
   gem.add_development_dependency "spring"
 end
diff --git a/dotenv.gemspec b/dotenv.gemspec
index d62f890..888faf0 100644
--- a/dotenv.gemspec
+++ b/dotenv.gemspec
@@ -2,17 +2,17 @@ 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.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 rails`
-  gem.files         = gem_files.split($OUTPUT_RECORD_SEPARATOR)
+  gem_files = `git ls-files README.md LICENSE lib bin | grep -v rails`
+  gem.files = gem_files.split($OUTPUT_RECORD_SEPARATOR)
   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 "rubocop", "~>0.40.0"
+  gem.add_development_dependency "standard"
 end
diff --git a/lib/dotenv.rb b/lib/dotenv.rb
index abf1441..18ddeeb 100644
--- a/lib/dotenv.rb
+++ b/lib/dotenv.rb
@@ -1,5 +1,6 @@
 require "dotenv/parser"
 require "dotenv/environment"
+require "dotenv/missing_keys"
 
 # The top level Dotenv module. The entrypoint for the application logic.
 module Dotenv
@@ -36,6 +37,23 @@ module Dotenv
     end
   end
 
+  # same as `overload`, but raises Errno::ENOENT if any files don't exist
+  def overload!(*filenames)
+    with(*filenames) do |f|
+      env = Environment.new(f, false)
+      instrument("dotenv.overload", env: env) { env.apply! }
+    end
+  end
+
+  # returns a hash of parsed key/value pairs but does not modify ENV
+  def parse(*filenames)
+    with(*filenames) do |f|
+      ignoring_nonexistent_files do
+        Environment.new(f, false)
+      end
+    end
+  end
+
   # Internal: Helper to expand list of filenames.
   #
   # Returns a hash of all the loaded environment variables.
@@ -55,6 +73,12 @@ module Dotenv
     end
   end
 
+  def require_keys(*keys)
+    missing_keys = keys.flatten - ::ENV.keys
+    return if missing_keys.empty?
+    raise MissingKeys, missing_keys
+  end
+
   def ignoring_nonexistent_files
     yield
   rescue Errno::ENOENT
diff --git a/lib/dotenv/cli.rb b/lib/dotenv/cli.rb
index 76e0225..1a586a5 100644
--- a/lib/dotenv/cli.rb
+++ b/lib/dotenv/cli.rb
@@ -1,36 +1,57 @@
 require "dotenv"
+require "dotenv/version"
+require "dotenv/template"
+require "optparse"
 
 module Dotenv
-  # The CLI is a class responsible of handling all the command line interface
-  # logic.
-  class CLI
-    attr_reader :argv
+  # The command line interface
+  class CLI < OptionParser
+    attr_reader :argv, :filenames, :overload
 
     def initialize(argv = [])
       @argv = argv.dup
+      @filenames = []
+      @overload = false
+
+      super "Usage: dotenv [options]"
+      separator ""
+
+      on("-f FILES", Array, "List of env files to parse") do |list|
+        @filenames = list
+      end
+
+      on("-o", "--overload", "override existing ENV variables") do
+        @overload = true
+      end
+
+      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
-      filenames = parse_filenames || []
-      begin
-        Dotenv.load!(*filenames)
-      rescue Errno::ENOENT => e
-        abort e.message
+      if @overload
+        Dotenv.overload!(*@filenames)
       else
-        exec(*argv) unless argv.empty?
+        Dotenv.load!(*@filenames)
       end
-    end
-
-    private
-
-    def parse_filenames
-      pos = argv.index("-f")
-      return nil unless pos
-      # drop the -f
-      argv.delete_at pos
-      # parse one or more comma-separated .env files
-      require "csv"
-      CSV.parse_line argv.delete_at(pos)
+    rescue Errno::ENOENT => e
+      abort e.message
+    else
+      exec(*@argv) unless @argv.empty?
     end
   end
 end
diff --git a/lib/dotenv/environment.rb b/lib/dotenv/environment.rb
index 90e6156..c7d21d0 100644
--- a/lib/dotenv/environment.rb
+++ b/lib/dotenv/environment.rb
@@ -4,12 +4,12 @@ module Dotenv
   class Environment < Hash
     attr_reader :filename
 
-    def initialize(filename, is_load)
+    def initialize(filename, is_load = false)
       @filename = filename
       load(is_load)
     end
 
-    def load(is_load)
+    def load(is_load = false)
       update Parser.call(read, is_load)
     end
 
diff --git a/lib/dotenv/missing_keys.rb b/lib/dotenv/missing_keys.rb
new file mode 100644
index 0000000..ecedcbf
--- /dev/null
+++ b/lib/dotenv/missing_keys.rb
@@ -0,0 +1,10 @@
+module Dotenv
+  class Error < StandardError; end
+
+  class MissingKeys < Error # :nodoc:
+    def initialize(keys)
+      key_word = "key#{keys.size > 1 ? "s" : ""}"
+      super("Missing required configuration #{key_word}: #{keys.inspect}")
+    end
+  end
+end
diff --git a/lib/dotenv/parser.rb b/lib/dotenv/parser.rb
index 4451f17..d2c8028 100644
--- a/lib/dotenv/parser.rb
+++ b/lib/dotenv/parser.rb
@@ -12,21 +12,21 @@ module Dotenv
       [Dotenv::Substitutions::Variable, Dotenv::Substitutions::Command]
 
     LINE = /
-      \A
-      \s*
-      (?:export\s+)?    # optional export
-      ([\w\.]+)         # key
-      (?:\s*=\s*|:\s+?) # separator
-      (                 # optional value begin
-        '(?:\'|[^'])*'  #   single quoted value
-        |               #   or
-        "(?:\"|[^"])*"  #   double quoted value
-        |               #   or
-        [^#\n]+         #   unquoted value
-      )?                # value end
-      \s*
-      (?:\#.*)?         # optional comment
-      \z
+      (?:^|\A)              # beginning of line
+      \s*                   # leading whitespace
+      (?:export\s+)?        # optional export
+      ([\w.]+)              # key
+      (?:\s*=\s*?|:\s+?)    # separator
+      (                     # optional value begin
+        \s*'(?:\\'|[^'])*'  #   single quoted value
+        |                   #   or
+        \s*"(?:\\"|[^"])*"  #   double quoted value
+        |                   #   or
+        [^\#\r\n]+          #   unquoted value
+      )?                    # value end
+      \s*                   # trailing whitespace
+      (?:\#.*)?             # optional comment
+      (?:$|\z)              # end of line
     /x
 
     class << self
@@ -44,7 +44,14 @@ module Dotenv
     end
 
     def call
-      @string.split(/[\n\r]+/).each do |line|
+      # Convert line breaks to same format
+      lines = @string.gsub(/\r\n?/, "\n")
+      # Process matches
+      lines.scan(LINE).each do |key, value|
+        @hash[key] = parse_value(value || "")
+      end
+      # Process non-matches
+      lines.gsub(LINE, "").split(/[\n\r]+/).each do |line|
         parse_line(line)
       end
       @hash
@@ -53,10 +60,7 @@ module Dotenv
     private
 
     def parse_line(line)
-      if (match = line.match(LINE))
-        key, value = match.captures
-        @hash[key] = parse_value(value || "")
-      elsif line.split.first == "export"
+      if line.split.first == "export"
         if variable_not_set?(line)
           raise FormatError, "Line #{line.inspect} has an unset variable"
         end
@@ -65,18 +69,10 @@ module Dotenv
 
     def parse_value(value)
       # Remove surrounding quotes
-      value = value.strip.sub(/\A(['"])(.*)\1\z/, '\2')
-
-      if Regexp.last_match(1) == '"'
-        value = unescape_characters(expand_newlines(value))
-      end
-
-      if Regexp.last_match(1) != "'"
-        self.class.substitutions.each do |proc|
-          value = proc.call(value, @hash, @is_load)
-        end
-      end
-      value
+      value = value.strip.sub(/\A(['"])(.*)\1\z/m, '\2')
+      maybe_quote = Regexp.last_match(1)
+      value = unescape_value(value, maybe_quote)
+      perform_substitutions(value, maybe_quote)
     end
 
     def unescape_characters(value)
@@ -90,5 +86,24 @@ module Dotenv
     def variable_not_set?(line)
       !line.split[1..-1].all? { |var| @hash.member?(var) }
     end
+
+    def unescape_value(value, maybe_quote)
+      if maybe_quote == '"'
+        unescape_characters(expand_newlines(value))
+      elsif maybe_quote.nil?
+        unescape_characters(value)
+      else
+        value
+      end
+    end
+
+    def perform_substitutions(value, maybe_quote)
+      if maybe_quote != "'"
+        self.class.substitutions.each do |proc|
+          value = proc.call(value, @hash, @is_load)
+        end
+      end
+      value
+    end
   end
 end
diff --git a/lib/dotenv/rails.rb b/lib/dotenv/rails.rb
index 5a733df..c799ffb 100644
--- a/lib/dotenv/rails.rb
+++ b/lib/dotenv/rails.rb
@@ -1,6 +1,6 @@
 require "dotenv"
 
-# Fix for rspec rake tasks loading in development
+# Fix for rake tasks loading in development
 #
 # Dotenv loads environment variables when the Rails application is initialized.
 # When running `rake`, the Rails application is initialized in development.
@@ -9,8 +9,11 @@ require "dotenv"
 #
 # See https://github.com/bkeepers/dotenv/issues/219
 if defined?(Rake.application)
-  is_running_specs = Rake.application.top_level_tasks.grep(/^spec(:|$)/).any?
-  Rails.env = ENV["RAILS_ENV"] ||= "test" if is_running_specs
+  task_regular_expression = /^(default$|parallel:spec|spec(:|$))/
+  if Rake.application.top_level_tasks.grep(task_regular_expression).any?
+    environment = Rake.application.options.show_tasks ? "development" : "test"
+    Rails.env = ENV["RAILS_ENV"] ||= environment
+  end
 end
 
 Dotenv.instrumenter = ActiveSupport::Notifications
@@ -38,6 +41,13 @@ module Dotenv
       Dotenv.load(*dotenv_files)
     end
 
+    # Public: Reload dotenv
+    #
+    # Same as `load`, but will override existing values in `ENV`
+    def overload
+      Dotenv.overload(*dotenv_files)
+    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.
@@ -51,8 +61,6 @@ module Dotenv
       instance.load
     end
 
-    config.before_configuration { load }
-
     private
 
     def dotenv_files
@@ -63,5 +71,7 @@ module Dotenv
         root.join(".env")
       ].compact
     end
+
+    config.before_configuration { load }
   end
 end
diff --git a/lib/dotenv/substitutions/command.rb b/lib/dotenv/substitutions/command.rb
index a0a8d61..724f87d 100644
--- a/lib/dotenv/substitutions/command.rb
+++ b/lib/dotenv/substitutions/command.rb
@@ -13,7 +13,7 @@ module Dotenv
           \$                  # literal $
           (?<cmd>             # collect command content for eval
             \(                # require opening paren
-            ([^()]|\g<cmd>)+  # allow any number of non-parens, or balanced
+            (?:[^()]|\g<cmd>)+  # allow any number of non-parens, or balanced
                               # parens (by nesting the <cmd> expression
                               # recursively)
             \)                # require closing paren
diff --git a/lib/dotenv/substitutions/variable.rb b/lib/dotenv/substitutions/variable.rb
index a7b7c1a..4dba441 100644
--- a/lib/dotenv/substitutions/variable.rb
+++ b/lib/dotenv/substitutions/variable.rb
@@ -19,11 +19,7 @@ module Dotenv
         /xi
 
         def call(value, env, is_load)
-          combined_env = if is_load
-                           env.merge(ENV)
-                         else
-                           ENV.to_h.merge(env)
-                         end
+          combined_env = is_load ? env.merge(ENV) : ENV.to_h.merge(env)
           value.gsub(VARIABLE) do |variable|
             match = $LAST_MATCH_INFO
             substitute(match, variable, combined_env)
@@ -33,7 +29,7 @@ module Dotenv
         private
 
         def substitute(match, variable, env)
-          if match[1] == '\\'
+          if match[1] == "\\"
             variable[1..-1]
           elsif match[3]
             env.fetch(match[3], "")
diff --git a/lib/dotenv/template.rb b/lib/dotenv/template.rb
new file mode 100644
index 0000000..1aa37a4
--- /dev/null
+++ b/lib/dotenv/template.rb
@@ -0,0 +1,26 @@
+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|
+            env_template.puts template_line(line)
+          end
+        end
+      end
+    end
+
+    def template_line(line)
+      var, value = line.split("=")
+      template = var.gsub(EXPORT_COMMAND, "")
+      is_a_comment = var.strip[0].eql?("#")
+      value.nil? || is_a_comment ? line : "#{var}=#{template}"
+    end
+  end
+end
diff --git a/lib/dotenv/version.rb b/lib/dotenv/version.rb
index 6bfe634..dbb95d0 100644
--- a/lib/dotenv/version.rb
+++ b/lib/dotenv/version.rb
@@ -1,3 +1,3 @@
 module Dotenv
-  VERSION = "2.4.0".freeze
+  VERSION = "2.8.1".freeze
 end
diff --git a/spec/dotenv/cli_spec.rb b/spec/dotenv/cli_spec.rb
index 0f2b6dc..012e525 100644
--- a/spec/dotenv/cli_spec.rb
+++ b/spec/dotenv/cli_spec.rb
@@ -38,6 +38,84 @@ describe "dotenv binary" do
     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 "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
+
   # Capture output to $stdout and $stderr
   def capture_output(&_block)
     original_stderr = $stderr
diff --git a/spec/dotenv/parser_spec.rb b/spec/dotenv/parser_spec.rb
index af7a06d..d611f22 100644
--- a/spec/dotenv/parser_spec.rb
+++ b/spec/dotenv/parser_spec.rb
@@ -9,6 +9,14 @@ describe Dotenv::Parser 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")
@@ -105,13 +113,11 @@ describe Dotenv::Parser do
 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
+  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.to raise_error(Dotenv::FormatError, 'Line "export OH_NO_NOT_SET" has an unset variable')
   end
 
   it "expands newlines in quoted strings" do
@@ -143,6 +149,10 @@ export OH_NO_NOT_SET')
     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
@@ -155,6 +165,20 @@ export OH_NO_NOT_SET')
     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")
@@ -169,6 +193,44 @@ export OH_NO_NOT_SET')
     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")
@@ -212,7 +274,7 @@ export OH_NO_NOT_SET')
 
     # This functionality is not supported on JRuby or Rubinius
     if (!defined?(RUBY_ENGINE) || RUBY_ENGINE != "jruby") &&
-       !defined?(Rubinius)
+        !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")
diff --git a/spec/dotenv/rails_spec.rb b/spec/dotenv/rails_spec.rb
index 4c511dc..2cb4ecb 100644
--- a/spec/dotenv/rails_spec.rb
+++ b/spec/dotenv/rails_spec.rb
@@ -2,20 +2,20 @@ require "spec_helper"
 require "rails"
 require "dotenv/rails"
 
-describe Dotenv::Railtie do
-  # Fake watcher for Spring
-  class SpecWatcher
-    attr_reader :items
+# Fake watcher for Spring
+class SpecWatcher
+  attr_reader :items
 
-    def initialize
-      @items = []
-    end
+  def initialize
+    @items = []
+  end
 
-    def add(*items)
-      @items |= items
-    end
+  def add(*items)
+    @items |= items
   end
+end
 
+describe Dotenv::Railtie do
   before do
     Rails.env = "test"
     allow(Rails).to receive(:root)
@@ -87,4 +87,46 @@ describe Dotenv::Railtie do
       end
     end
   end
+
+  context "overload" do
+    before { Dotenv::Railtie.overload }
+
+    it "does not load .env.local in test rails environment" do
+      expect(Dotenv::Railtie.instance.send(:dotenv_files)).to eql(
+        [
+          Rails.root.join(".env.test.local"),
+          Rails.root.join(".env.test"),
+          Rails.root.join(".env")
+        ]
+      )
+    end
+
+    it "does load .env.local in development environment" do
+      Rails.env = "development"
+      expect(Dotenv::Railtie.instance.send(:dotenv_files)).to eql(
+        [
+          Rails.root.join(".env.development.local"),
+          Rails.root.join(".env.local"),
+          Rails.root.join(".env.development"),
+          Rails.root.join(".env")
+        ]
+      )
+    end
+
+    it "overloads .env.test with .env" do
+      expect(ENV["DOTENV"]).to eql("true")
+    end
+
+    context "when loading a file containing already set variables" do
+      subject { Dotenv::Railtie.overload }
+
+      it "overrides any existing ENV variables" do
+        ENV["DOTENV"] = "predefined"
+
+        expect do
+          subject
+        end.to(change { ENV["DOTENV"] }.from("predefined").to("true"))
+      end
+    end
+  end
 end
diff --git a/spec/dotenv_spec.rb b/spec/dotenv_spec.rb
index 4940a2f..ea80e2f 100644
--- a/spec/dotenv_spec.rb
+++ b/spec/dotenv_spec.rb
@@ -10,8 +10,7 @@ describe Dotenv do
       let(:env_files) { [] }
 
       it "defaults to .env" do
-        expect(Dotenv::Environment).to receive(:new).with(expand(".env"),
-                                                          anything)
+        expect(Dotenv::Environment).to receive(:new).with(expand(".env"), anything)
           .and_return(double(apply: {}, apply!: {}))
         subject
       end
@@ -33,13 +32,13 @@ describe Dotenv 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" }
+        {"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
@@ -131,6 +130,36 @@ describe Dotenv do
     end
   end
 
+  describe "overload!" do
+    let(:env_files) { [fixture_path("plain.env")] }
+    subject { Dotenv.overload!(*env_files) }
+    it_behaves_like "load"
+
+    it "initializes the Environment with a falsey is_load" do
+      expect(Dotenv::Environment).to receive(:new).with(anything, false)
+        .and_return(double(apply: {}, apply!: {}))
+      subject
+    end
+
+    context "when loading a file containing already set variables" do
+      let(:env_files) { [fixture_path("plain.env")] }
+
+      it "overrides 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 }
@@ -153,6 +182,84 @@ describe Dotenv do
     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
+  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 a falsey is_load" do
+      expect(Dotenv::Environment).to receive(:new).with(anything, 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") }
 
@@ -161,7 +268,7 @@ describe Dotenv do
     end
 
     it "fixture file has UTF-8 BOM" do
-      contents = File.open(subject, "rb", &:read).force_encoding("UTF-8")
+      contents = File.binread(subject).force_encoding("UTF-8")
       expect(contents).to start_with("\xEF\xBB\xBF".force_encoding("UTF-8"))
     end
   end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index bb72692..a0accd2 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -3,7 +3,7 @@ require "dotenv"
 RSpec.configure do |config|
   # Restore the state of ENV after each spec
   config.before { @env_keys = ENV.keys }
-  config.after  { ENV.delete_if { |k, _v| !@env_keys.include?(k) } }
+  config.after { ENV.delete_if { |k, _v| !@env_keys.include?(k) } }
 end
 
 def fixture_path(name)

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv-rails.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/cli.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/environment.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/load.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/missing_keys.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/parser.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/rails-now.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/rails.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/substitutions/command.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/substitutions/variable.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/tasks.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/template.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/lib/dotenv/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/dotenv-2.8.1.gemspec
-rwxr-xr-x  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.8.1/bin/dotenv

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv-rails.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/cli.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/environment.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/load.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/parser.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/rails-now.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/rails.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/substitutions/command.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/substitutions/variable.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/tasks.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/lib/dotenv/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/dotenv-2.4.0.gemspec
-rwxr-xr-x  root/root   /usr/share/rubygems-integration/all/gems/dotenv-2.4.0/bin/dotenv

No differences were encountered in the control files

More details

Full run details