Full Code of bkeepers/dotenv for AI

main 34156bf400cd cached
53 files
79.2 KB
23.2k tokens
93 symbols
1 requests
Download .txt
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:

<table>
  <thead>
    <tr>
      <th>Priority</th>
      <th colspan="3">Environment</th>
      <th><code>.gitignore</code>it?</th>
      <th>Notes</th>
    </tr>
    <tr>
      <th></th>
      <th>development</th>
      <th>test</th>
      <th>production</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tr>
    <td>highest</td>
    <td><code>.env.development.local</code></td>
    <td><code>.env.test.local</code></td>
    <td><code>.env.production.local</code></td>
    <td>Yes</td>
    <td>Environment-specific local overrides</td>
  </tr>
  <tr>
    <td>2nd</td>
    <td><code>.env.local</code></td>
    <td><strong>N/A</strong></td>
    <td><code>.env.local</code></td>
    <td>Yes</td>
    <td>Local overrides</td>
  </tr>
  <tr>
    <td>3rd</td>
    <td><code>.env.development</code></td>
    <td><code>.env.test</code></td>
    <td><code>.env.production</code></td>
    <td>No</td>
    <td>Shared environment-specific variables</td>
  </tr>
  <tr>
    <td>last</td>
    <td><code>.env</code></td>
    <td><code>.env</code></td>
    <td><code>.env</code></td>
    <td><a href="#should-i-commit-my-env-file">Maybe</a></td>
    <td>Shared for all environments</td>
  </tr>
</table>


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.<that environment's name>`.

### 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>export\s+)?   # optional export
      (?<key>[\w.]+)          # key
      (?:                     # optional separator and value
        (?:\s*=\s*?|:\s+?)    #   separator
        (?<value>             #   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 = /
          (?<backslash>\\)?   # is it escaped with a backslash?
          \$                  # literal $
          (?<cmd>             # collect command content for eval
            \(                # require opening paren
            (?:[^()]|\g<cmd>)+  # allow any number of non-parens, or balanced
                              # parens (by nesting the <cmd> 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<String>] Files to parse
  # @param overwrite [Boolean] Overwrite existing `ENV` values
  # @param ignore [Boolean] Ignore non-existent files
  # @param block [Proc] Block to yield for each parsed `Dotenv::Environment`
  # @return [Hash] parsed key/value pairs
  def parse(*filenames, overwrite: false, ignore: true, &block)
    filenames << ".env" if filenames.empty?
    filenames = filenames.reverse if overwrite

    filenames.reduce({}) do |hash, filename|
      begin
        env = Environment.new(File.expand_path(filename), overwrite: overwrite)
        env = block.call(env) if block
      rescue Errno::ENOENT, Errno::EISDIR
        raise unless ignore
      end

      hash.merge! env || {}
    end
  end

  # Save the current `ENV` to be restored later
  def save
    instrument(:save) do |payload|
      @diff = payload[:diff] = Dotenv::Diff.new
    end
  end

  # Restore `ENV` to a given state
  #
  # @param env [Hash] Hash of keys and values to restore, defaults to the last saved state
  # @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error.
  def restore(env = @diff&.a, safe: Thread.current == Thread.main)
    # No previously saved or provided state to restore
    return unless env

    diff = Dotenv::Diff.new(b: env)
    return unless diff.any?

    unless safe
      raise ThreadError, <<~EOE.tr("\n", " ")
        Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration
        of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore
        this error.
      EOE
    end
    instrument(:restore, diff: diff) { ENV.replace(env) }
  end

  # Update `ENV` with the given hash of keys and values
  #
  # @param env [Hash] Hash of keys and values to set in `ENV`
  # @param overwrite [Boolean|: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
Download .txt
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
Download .txt
SYMBOL INDEX (93 symbols across 20 files)

FILE: lib/dotenv.rb
  type Dotenv (line 8) | module Dotenv
    function load (line 18) | def load(*filenames, overwrite: false, ignore: true)
    function load! (line 27) | def load!(*filenames)
    function overwrite (line 32) | def overwrite(*filenames)
    function overwrite! (line 38) | def overwrite!(*filenames)
    function parse (line 50) | def parse(*filenames, overwrite: false, ignore: true, &block)
    function save (line 67) | def save
    function restore (line 77) | def restore(env = @diff&.a, safe: Thread.current == Thread.main)
    function update (line 98) | def update(env = {}, overwrite: false)
    function modify (line 124) | def modify(env = {}, &block)
    function require_keys (line 134) | def require_keys(*keys)
    function instrument (line 142) | def instrument(name, payload = {}, &block)

FILE: lib/dotenv/cli.rb
  type Dotenv (line 6) | module Dotenv
    class CLI (line 8) | class CLI < OptionParser
      method initialize (line 11) | def initialize(argv = [])
      method run (line 51) | def run

FILE: lib/dotenv/diff.rb
  type Dotenv (line 1) | module Dotenv
    class Diff (line 3) | class Diff
      method initialize (line 16) | def initialize(a: snapshot, b: ENV, &block)
      method added (line 24) | def added
      method removed (line 29) | def removed
      method changed (line 34) | def changed
      method env (line 41) | def env
      method any? (line 46) | def any?
      method snapshot (line 52) | def snapshot

FILE: lib/dotenv/environment.rb
  type Dotenv (line 1) | module Dotenv
    class Environment (line 3) | class Environment < Hash
      method initialize (line 10) | def initialize(filename, overwrite: false)
      method load (line 17) | def load
      method read (line 21) | def read

FILE: lib/dotenv/log_subscriber.rb
  type Dotenv (line 3) | module Dotenv
    class LogSubscriber (line 11) | class LogSubscriber < ActiveSupport::LogSubscriber
      method logger (line 14) | def logger
      method load (line 18) | def load(event)
      method update (line 24) | def update(event)
      method save (line 30) | def save(event)
      method restore (line 34) | def restore(event)
      method color_filename (line 49) | def color_filename(filename)
      method color_var (line 53) | def color_var(name)
      method color_env_constant (line 57) | def color_env_constant

FILE: lib/dotenv/missing_keys.rb
  type Dotenv (line 1) | module Dotenv
    class Error (line 2) | class Error < StandardError; end
    class MissingKeys (line 4) | class MissingKeys < Error # :nodoc:
      method initialize (line 5) | def initialize(keys)

FILE: lib/dotenv/parser.rb
  type Dotenv (line 4) | module Dotenv
    class FormatError (line 6) | class FormatError < SyntaxError; end
    class Parser (line 10) | class Parser
      method call (line 41) | def call(...)
      method initialize (line 46) | def initialize(string, overwrite: false)
      method call (line 53) | def call
      method existing? (line 76) | def existing?(key)
      method parse_value (line 80) | def parse_value(value)
      method unescape_characters (line 97) | def unescape_characters(value)
      method expand_newlines (line 101) | def expand_newlines(value)

FILE: lib/dotenv/rails.rb
  type Dotenv (line 20) | module Dotenv
    class Rails (line 22) | class Rails < ::Rails::Railtie
      method initialize (line 25) | def initialize
      method load (line 45) | def load
      method overload (line 49) | def overload
      method root (line 57) | def root
      method logger= (line 62) | def logger=(new_logger)
      method env (line 73) | def env
      method deprecator (line 83) | def deprecator # :nodoc:
      method load (line 89) | def self.load

FILE: lib/dotenv/replay_logger.rb
  type Dotenv (line 1) | module Dotenv
    class ReplayLogger (line 3) | class ReplayLogger < Logger
      method initialize (line 4) | def initialize
      method add (line 10) | def add(*args, &block)
      method replay (line 15) | def replay(logger)

FILE: lib/dotenv/substitutions/command.rb
  type Dotenv (line 3) | module Dotenv
    type Substitutions (line 4) | module Substitutions
      type Command (line 9) | module Command
        function call (line 23) | def call(value, env)

FILE: lib/dotenv/substitutions/variable.rb
  type Dotenv (line 3) | module Dotenv
    type Substitutions (line 4) | module Substitutions
      type Variable (line 10) | module Variable
        function call (line 21) | def call(value, env)

FILE: lib/dotenv/template.rb
  type Dotenv (line 1) | module Dotenv
    class EnvTemplate (line 4) | class EnvTemplate
      method initialize (line 5) | def initialize(env_file)
      method create_template (line 9) | def create_template
      method is_comment? (line 31) | def is_comment?(line)
      method var_defined? (line 35) | def var_defined?(line)
      method line_blank? (line 40) | def line_blank?(line)

FILE: lib/dotenv/version.rb
  type Dotenv (line 1) | module Dotenv

FILE: spec/dotenv/cli_spec.rb
  function run (line 9) | def run(*args)

FILE: spec/dotenv/environment_spec.rb
  function env (line 20) | def env(text, ...)

FILE: spec/dotenv/parser_spec.rb
  function env (line 4) | def env(...)

FILE: spec/dotenv/rails_spec.rb
  function stub_spring (line 217) | def stub_spring(load_watcher: true)

FILE: spec/dotenv_spec.rb
  function expand (line 427) | def expand(path)

FILE: spec/spec_helper.rb
  function fixture_path (line 5) | def fixture_path(*parts)
  function capture_output (line 10) | def capture_output(&_block)

FILE: test/autorestore_test.rb
  class AutorestoreTest (line 8) | class AutorestoreTest < ActiveSupport::TestCase
Condensed preview — 53 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (89K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 209,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"bundler\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  - packa"
  },
  {
    "path": ".github/funding.yml",
    "chars": 19,
    "preview": "github: [bkeepers]\n"
  },
  {
    "path": ".github/issue_template.md",
    "chars": 402,
    "preview": "### Steps to reproduce\nTell us how to reproduce the issue.\nShow how you included dotenv (Gemfile).\nPaste your env using:"
  },
  {
    "path": ".github/stale.yml",
    "chars": 684,
    "preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a "
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 1605,
    "preview": "name: CI\non:\n  push:\n    branches: [main]\n  pull_request:\n  schedule:\n    - cron: \"0 0 * * *\" # Once/day\n\njobs:\n  versio"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 463,
    "preview": "name: Publish Gem\non:\n  release:\n    types: [published]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n     "
  },
  {
    "path": ".gitignore",
    "chars": 84,
    "preview": "*.gem\n*.rbc\n.bundle\n.config\n.ruby-version\n.yardoc\nGemfile.lock\ntmp\nvendor\n.DS_Store\n"
  },
  {
    "path": ".standard.yml",
    "chars": 83,
    "preview": "ruby_version: 3.0\n\nignore:\n  - lib/dotenv/parser.rb:\n      - Lint/InheritException\n"
  },
  {
    "path": "Changelog.md",
    "chars": 218,
    "preview": "See [Releases](https://github.com/bkeepers/dotenv/releases) for the latest releases and changelogs.\n\n[View older release"
  },
  {
    "path": "Gemfile",
    "chars": 255,
    "preview": "source \"https://rubygems.org\"\ngemspec name: \"dotenv\"\ngemspec name: \"dotenv-rails\"\n\ngem \"railties\", \"~> #{ENV[\"RAILS_VERS"
  },
  {
    "path": "Guardfile",
    "chars": 227,
    "preview": "guard \"bundler\" do\n  watch(\"Gemfile\")\nend\n\nguard \"rspec\", cmd: \"bundle exec rspec\" do\n  watch(%r{^spec/.+_spec\\.rb$})\n  "
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "Copyright (c) 2012 Brandon Keepers\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na"
  },
  {
    "path": "OWNERS",
    "chars": 129,
    "preview": "# This project is maintained by:\n@bkeepers\n\n# For more information on the OWNERS file, see:\n# https://github.com/bkeeper"
  },
  {
    "path": "README.md",
    "chars": 11852,
    "preview": "# dotenv [![Gem Version](https://badge.fury.io/rb/dotenv.svg)](https://badge.fury.io/rb/dotenv)\n\nShim to load environmen"
  },
  {
    "path": "Rakefile",
    "chars": 825,
    "preview": "#!/usr/bin/env rake\n\nrequire \"bundler/gem_helper\"\nrequire \"rspec/core/rake_task\"\nrequire \"rake/testtask\"\nrequire \"standa"
  },
  {
    "path": "benchmark/parse_ips.rb",
    "chars": 385,
    "preview": "require \"bundler/setup\"\nrequire \"dotenv\"\nrequire \"benchmark/ips\"\nrequire \"tempfile\"\n\nf = Tempfile.create(\"benchmark_ips."
  },
  {
    "path": "benchmark/parse_profile.rb",
    "chars": 464,
    "preview": "require \"bundler/setup\"\nrequire \"dotenv\"\nrequire \"stackprof\"\nrequire \"benchmark/ips\"\nrequire \"tempfile\"\n\nf = Tempfile.cr"
  },
  {
    "path": "bin/dotenv",
    "chars": 68,
    "preview": "#!/usr/bin/env ruby\n\nrequire \"dotenv/cli\"\nDotenv::CLI.new(ARGV).run\n"
  },
  {
    "path": "dotenv-rails.gemspec",
    "chars": 742,
    "preview": "require File.expand_path(\"../lib/dotenv/version\", __FILE__)\nrequire \"English\"\n\nGem::Specification.new \"dotenv-rails\", Do"
  },
  {
    "path": "dotenv.gemspec",
    "chars": 858,
    "preview": "require File.expand_path(\"../lib/dotenv/version\", __FILE__)\nrequire \"English\"\n\nGem::Specification.new \"dotenv\", Dotenv::"
  },
  {
    "path": "lib/dotenv/autorestore.rb",
    "chars": 801,
    "preview": "# Automatically restore `ENV` to its original state after\n\nif defined?(RSpec.configure)\n  RSpec.configure do |config|\n  "
  },
  {
    "path": "lib/dotenv/cli.rb",
    "chars": 1378,
    "preview": "require \"dotenv\"\nrequire \"dotenv/version\"\nrequire \"dotenv/template\"\nrequire \"optparse\"\n\nmodule Dotenv\n  # The `dotenv` c"
  },
  {
    "path": "lib/dotenv/diff.rb",
    "chars": 1801,
    "preview": "module Dotenv\n  # A diff between multiple states of ENV.\n  class Diff\n    # The initial state\n    attr_reader :a\n\n    # "
  },
  {
    "path": "lib/dotenv/environment.rb",
    "chars": 632,
    "preview": "module Dotenv\n  # A `.env` file that will be read and parsed into a Hash\n  class Environment < Hash\n    attr_reader :fil"
  },
  {
    "path": "lib/dotenv/load.rb",
    "chars": 77,
    "preview": "require \"dotenv\"\n\ndefined?(Dotenv::Rails) ? Dotenv::Rails.load : Dotenv.load\n"
  },
  {
    "path": "lib/dotenv/log_subscriber.rb",
    "chars": 1473,
    "preview": "require \"active_support/log_subscriber\"\n\nmodule Dotenv\n  # Logs instrumented events\n  #\n  # Usage:\n  #   require \"active"
  },
  {
    "path": "lib/dotenv/missing_keys.rb",
    "chars": 252,
    "preview": "module Dotenv\n  class Error < StandardError; end\n\n  class MissingKeys < Error # :nodoc:\n    def initialize(keys)\n      k"
  },
  {
    "path": "lib/dotenv/parser.rb",
    "chars": 3334,
    "preview": "require \"dotenv/substitutions/variable\"\nrequire \"dotenv/substitutions/command\" if RUBY_VERSION > \"1.8.7\"\n\nmodule Dotenv\n"
  },
  {
    "path": "lib/dotenv/rails-now.rb",
    "chars": 414,
    "preview": "# If you use gems that require environment variables to be set before they are\n# loaded, then list `dotenv` in the `Gemf"
  },
  {
    "path": "lib/dotenv/rails.rb",
    "chars": 3971,
    "preview": "# Since rubygems doesn't support optional dependencies, we have to manually check\nunless Gem::Requirement.new(\">= 6.1\")."
  },
  {
    "path": "lib/dotenv/replay_logger.rb",
    "chars": 553,
    "preview": "module Dotenv\n  # A logger that can be used before the apps real logger is initialized.\n  class ReplayLogger < Logger\n  "
  },
  {
    "path": "lib/dotenv/substitutions/command.rb",
    "chars": 1298,
    "preview": "require \"English\"\n\nmodule Dotenv\n  module Substitutions\n    # Substitute shell commands in a value.\n    #\n    #   SHA=$("
  },
  {
    "path": "lib/dotenv/substitutions/variable.rb",
    "chars": 884,
    "preview": "require \"English\"\n\nmodule Dotenv\n  module Substitutions\n    # Substitute variables in a value.\n    #\n    #   HOST=exampl"
  },
  {
    "path": "lib/dotenv/tasks.rb",
    "chars": 123,
    "preview": "desc \"Load environment settings from .env\"\ntask :dotenv do\n  require \"dotenv\"\n  Dotenv.load\nend\n\ntask environment: :dote"
  },
  {
    "path": "lib/dotenv/template.rb",
    "chars": 1053,
    "preview": "module Dotenv\n  EXPORT_COMMAND = \"export \".freeze\n  # Class for creating a template from a env file\n  class EnvTemplate\n"
  },
  {
    "path": "lib/dotenv/version.rb",
    "chars": 45,
    "preview": "module Dotenv\n  VERSION = \"3.2.0\".freeze\nend\n"
  },
  {
    "path": "lib/dotenv-rails.rb",
    "chars": 17,
    "preview": "require \"dotenv\"\n"
  },
  {
    "path": "lib/dotenv.rb",
    "chars": 4854,
    "preview": "require \"dotenv/version\"\nrequire \"dotenv/parser\"\nrequire \"dotenv/environment\"\nrequire \"dotenv/missing_keys\"\nrequire \"dot"
  },
  {
    "path": "spec/dotenv/cli_spec.rb",
    "chars": 4989,
    "preview": "require \"spec_helper\"\nrequire \"dotenv/cli\"\n\ndescribe \"dotenv binary\" do\n  before do\n    Dir.chdir(File.expand_path(\"../."
  },
  {
    "path": "spec/dotenv/diff_spec.rb",
    "chars": 1389,
    "preview": "require \"spec_helper\"\n\ndescribe Dotenv::Diff do\n  let(:before) { {} }\n  let(:after) { {} }\n  subject { Dotenv::Diff.new("
  },
  {
    "path": "spec/dotenv/environment_spec.rb",
    "chars": 612,
    "preview": "require \"spec_helper\"\n\ndescribe Dotenv::Environment do\n  subject { env(\"OPTION_A=1\\nOPTION_B=2\") }\n\n  describe \"initiali"
  },
  {
    "path": "spec/dotenv/log_subscriber_spec.rb",
    "chars": 2010,
    "preview": "require \"spec_helper\"\nrequire \"active_support/all\"\nrequire \"rails\"\nrequire \"dotenv/rails\"\n\ndescribe Dotenv::LogSubscribe"
  },
  {
    "path": "spec/dotenv/parser_spec.rb",
    "chars": 9531,
    "preview": "require \"spec_helper\"\n\ndescribe Dotenv::Parser do\n  def env(...)\n    Dotenv::Parser.call(...)\n  end\n\n  it \"parses unquot"
  },
  {
    "path": "spec/dotenv/rails_spec.rb",
    "chars": 6324,
    "preview": "require \"spec_helper\"\nrequire \"rails\"\nrequire \"dotenv/rails\"\n\ndescribe Dotenv::Rails do\n  let(:log_output) { StringIO.ne"
  },
  {
    "path": "spec/dotenv_spec.rb",
    "chars": 11423,
    "preview": "require \"spec_helper\"\n\ndescribe Dotenv do\n  before do\n    Dir.chdir(File.expand_path(\"../fixtures\", __FILE__))\n  end\n\n  "
  },
  {
    "path": "spec/fixtures/bom.env",
    "chars": 10,
    "preview": "BOM=UTF-8"
  },
  {
    "path": "spec/fixtures/exported.env",
    "chars": 39,
    "preview": "export OPTION_A=2\nexport OPTION_B='\\n'\n"
  },
  {
    "path": "spec/fixtures/important.env",
    "chars": 35,
    "preview": "PLAIN=false\nOPTION_A=abc\nOPTION_B=2"
  },
  {
    "path": "spec/fixtures/plain.env",
    "chars": 70,
    "preview": "PLAIN=true\nOPTION_A=1\nOPTION_B=2\nOPTION_C= 3\nOPTION_D =4\nOPTION_E = 5\n"
  },
  {
    "path": "spec/fixtures/quoted.env",
    "chars": 116,
    "preview": "QUOTED=true\nOPTION_A='1'\nOPTION_B='2'\nOPTION_C=''\nOPTION_D='\\n'\nOPTION_E=\"1\"\nOPTION_F=\"2\"\nOPTION_G=\"\"\nOPTION_H=\"\\n\"\n"
  },
  {
    "path": "spec/fixtures/yaml.env",
    "chars": 54,
    "preview": "OPTION_A: 1\nOPTION_B: '2'\nOPTION_C: ''\nOPTION_D: '\\n'\n"
  },
  {
    "path": "spec/spec_helper.rb",
    "chars": 404,
    "preview": "require \"dotenv\"\nrequire \"dotenv/autorestore\"\nrequire \"tmpdir\"\n\ndef fixture_path(*parts)\n  Pathname.new(__dir__).join(\"."
  },
  {
    "path": "test/autorestore_test.rb",
    "chars": 516,
    "preview": "require \"active_support\" # Rails 6.1 fails if this is not loaded\nrequire \"active_support/test_case\"\nrequire \"minitest/au"
  }
]

About this extraction

This page contains the full source code of the bkeepers/dotenv GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 53 files (79.2 KB), approximately 23.2k tokens, and a symbol index with 93 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!