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 [](https://badge.fury.io/rb/dotenv)
Shim to load environment variables from `.env` into `ENV` in *development*.
Storing [configuration in the environment](http://12factor.net/config) is one of the tenets of a [twelve-factor app](http://12factor.net). Anything that is likely to change between deployment environments–such as resource handles for databases or credentials for external services–should be extracted from the code into environment variables.
But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. dotenv loads variables from a `.env` file into `ENV` when the environment is bootstrapped.
## Installation
Add this line to the top of your application's Gemfile and run `bundle install`:
```ruby
gem 'dotenv', groups: [:development, :test]
```
## Usage
Add your application configuration to your `.env` file in the root of your project:
```shell
S3_BUCKET=YOURS3BUCKET
SECRET_KEY=YOURSECRETKEYGOESHERE
```
Whenever your application loads, these variables will be available in `ENV`:
```ruby
config.fog_directory = ENV['S3_BUCKET']
```
See the [API Docs](https://rubydoc.info/github/bkeepers/dotenv/main) for more.
### Rails
Dotenv will automatically load when your Rails app boots. See [Customizing Rails](#customizing-rails) to change which files are loaded and when.
### Sinatra / Ruby
Load Dotenv as early as possible in your application bootstrap process:
```ruby
require 'dotenv/load'
# or
require 'dotenv'
Dotenv.load
```
By default, `load` will look for a file called `.env` in the current working directory.
Pass in multiple files and they will be loaded in order.
The first value set for a variable will win.
Existing environment variables will not be overwritten unless you set `overwrite: true`.
```ruby
require 'dotenv'
Dotenv.load('file1.env', 'file2.env')
```
### Autorestore in tests
Since 3.0, dotenv in a Rails app will automatically restore `ENV` after each test. This means you can modify `ENV` in your tests without fear of leaking state to other tests. It works with both `ActiveSupport::TestCase` and `Rspec`.
To disable this behavior, set `config.dotenv.autorestore = false` in `config/application.rb` or `config/environments/test.rb`. It is disabled by default if your app uses [climate_control](https://github.com/thoughtbot/climate_control) or [ice_age](https://github.com/dpep/ice_age_rb).
To use this behavior outside of a Rails app, just `require "dotenv/autorestore"` in your test suite.
See [`Dotenv.save`](https://rubydoc.info/github/bkeepers/dotenv/main/Dotenv:save), [Dotenv.restore](https://rubydoc.info/github/bkeepers/dotenv/main/Dotenv:restore), and [`Dotenv.modify(hash) { ... }`](https://rubydoc.info/github/bkeepers/dotenv/main/Dotenv:modify) for manual usage.
### Rake
To ensure `.env` is loaded in rake, load the tasks:
```ruby
require 'dotenv/tasks'
task mytask: :dotenv do
# things that require .env
end
```
### CLI
You can use the `dotenv` executable load `.env` before launching your application:
```console
$ dotenv ./script.rb
```
The `dotenv` executable also accepts the flag `-f`. Its value should be a comma-separated list of configuration files, in the order of the most important to the least important. All of the files must exist. There _must_ be a space between the flag and its value.
```console
$ dotenv -f ".env.local,.env" ./script.rb
```
The `dotenv` executable can optionally ignore missing files with the `-i` or `--ignore` flag. For example, if the `.env.local` file does not exist, the following will ignore the missing file and only load the `.env` file.
```console
$ dotenv -i -f ".env.local,.env" ./script.rb
```
### Load Order
If you use gems that require environment variables to be set before they are loaded, then list `dotenv` in the `Gemfile` before those other gems and require `dotenv/load`.
```ruby
gem 'dotenv', require: 'dotenv/load'
gem 'gem-that-requires-env-variables'
```
### Customizing Rails
Dotenv will load the following files depending on `RAILS_ENV`, with the first file having the highest precedence, and `.env` having the lowest precedence:
| Priority |
Environment |
.gitignoreit? |
Notes |
|
development |
test |
production |
|
|
| highest |
.env.development.local |
.env.test.local |
.env.production.local |
Yes |
Environment-specific local overrides |
| 2nd |
.env.local |
N/A |
.env.local |
Yes |
Local overrides |
| 3rd |
.env.development |
.env.test |
.env.production |
No |
Shared environment-specific variables |
| last |
.env |
.env |
.env |
Maybe |
Shared for all environments |
These files are loaded during the `before_configuration` callback, which is fired when the `Application` constant is defined in `config/application.rb` with `class Application < Rails::Application`. If you need it to be initialized sooner, or need to customize the loading process, you can do so at the top of `application.rb`
```ruby
# config/application.rb
Bundler.require(*Rails.groups)
# Load .env.local in test
Dotenv::Rails.files.unshift(".env.local") if ENV["RAILS_ENV"] == "test"
module YourApp
class Application < Rails::Application
# ...
end
end
```
Available options:
* `Dotenv::Rails.files` - list of files to be loaded, in order of precedence.
* `Dotenv::Rails.overwrite` - Overwrite existing `ENV` variables with contents of `.env*` files
* `Dotenv::Rails.logger` - The logger to use for dotenv's logging. Defaults to `Rails.logger`
* `Dotenv::Rails.autorestore` - Enable or disable [autorestore](#autorestore-in-tests)
### Multi-line values
Multi-line values with line breaks must be surrounded with double quotes.
```shell
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...
HkVN9...
...
-----END DSA PRIVATE KEY-----"
```
Prior to 3.0, dotenv would replace `\n` in quoted strings with a newline, but that behavior is deprecated. To use the old behavior, set `DOTENV_LINEBREAK_MODE=legacy` before any variables that include `\n`:
```shell
DOTENV_LINEBREAK_MODE=legacy
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nHkVN9...\n-----END DSA PRIVATE KEY-----\n"
```
### Command Substitution
You need to add the output of a command in one of your variables? Simply add it with `$(your_command)`:
```shell
DATABASE_URL="postgres://$(whoami)@localhost/my_database"
```
### Variable Substitution
You need to add the value of another variable in one of your variables? You can reference the variable with `${VAR}` or often just `$VAR` in unquoted or double-quoted values.
```shell
DATABASE_URL="postgres://${USER}@localhost/my_database"
```
If a value contains a `$` and it is not intended to be a variable, wrap it in single quotes.
```shell
PASSWORD='pas$word'
```
### Comments
Comments may be added to your file as such:
```shell
# This is a comment
SECRET_KEY=YOURSECRETKEYGOESHERE # comment
SECRET_HASH="something-with-a-#-hash"
```
### Exports
For compatability, you may also add `export` in front of each line so you can `source` the file in bash:
```shell
export S3_BUCKET=YOURS3BUCKET
export SECRET_KEY=YOURSECRETKEYGOESHERE
```
### Required Keys
If a particular configuration value is required but not set, it's appropriate to raise an error.
To require configuration keys:
```ruby
# config/initializers/dotenv.rb
Dotenv.require_keys("SERVICE_APP_ID", "SERVICE_KEY", "SERVICE_SECRET")
```
If any of the configuration keys above are not set, your application will raise an error during initialization. This method is preferred because it prevents runtime errors in a production application due to improper configuration.
### Parsing
To parse a list of env files for programmatic inspection without modifying the ENV:
```ruby
Dotenv.parse(".env.local", ".env")
# => {'S3_BUCKET' => 'YOURS3BUCKET', 'SECRET_KEY' => 'YOURSECRETKEYGOESHERE', ...}
```
This method returns a hash of the ENV var name/value pairs.
### Templates
You can use the `-t` or `--template` flag on the dotenv cli to create a template of your `.env` file.
```console
$ dotenv -t .env
```
A template will be created in your working directory named `{FILENAME}.template`. So in the above example, it would create a `.env.template` file.
The template will contain all the environment variables in your `.env` file but with their values set to the variable names.
```shell
# .env
S3_BUCKET=YOURS3BUCKET
SECRET_KEY=YOURSECRETKEYGOESHERE
```
Would become
```shell
# .env.template
S3_BUCKET=S3_BUCKET
SECRET_KEY=SECRET_KEY
```
## Frequently Answered Questions
### Can I use dotenv in production?
dotenv was originally created to load configuration variables into `ENV` in *development*. There are typically better ways to manage configuration in production environments - such as `/etc/environment` managed by [Puppet](https://github.com/puppetlabs/puppet) or [Chef](https://github.com/chef/chef), `heroku config`, etc.
However, some find dotenv to be a convenient way to configure Rails applications in staging and production environments, and you can do that by defining environment-specific files like `.env.production` or `.env.test`.
If you use this gem to handle env vars for multiple Rails environments (development, test, production, etc.), please note that env vars that are general to all environments should be stored in `.env`. Then, environment specific env vars should be stored in `.env.`.
### Should I commit my .env file?
Credentials should only be accessible on the machines that need access to them. Never commit sensitive information to a repository that is not needed by every development machine and server.
Personally, I prefer to commit the `.env` file with development-only settings. This makes it easy for other developers to get started on the project without compromising credentials for other environments. If you follow this advice, make sure that all the credentials for your development environment are different from your other deployments and that the development credentials do not have access to any confidential data.
### Why is it not overwriting existing `ENV` variables?
By default, it **won't** overwrite existing environment variables as dotenv assumes the deployment environment has more knowledge about configuration than the application does. To overwrite existing environment variables you can use `Dotenv.load files, overwrite: true`.
To warn when a value was not overwritten (e.g. to make users aware of this gotcha),
use `Dotenv.load files, overwrite: :warn`.
You can also use the `-o` or `--overwrite` flag on the dotenv cli to overwrite existing `ENV` variables.
```console
$ dotenv -o -f ".env.local,.env"
```
## Contributing
If you want a better idea of how dotenv works, check out the [Ruby Rogues Code Reading of dotenv](https://www.youtube.com/watch?v=lKmY_0uY86s).
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Added some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
================================================
FILE: Rakefile
================================================
#!/usr/bin/env rake
require "bundler/gem_helper"
require "rspec/core/rake_task"
require "rake/testtask"
require "standard/rake"
namespace "dotenv" do
Bundler::GemHelper.install_tasks name: "dotenv"
end
class DotenvRailsGemHelper < Bundler::GemHelper
def guard_already_tagged
# noop
end
def tag_version
# noop
end
end
namespace "dotenv-rails" do
DotenvRailsGemHelper.install_tasks name: "dotenv-rails"
end
task build: ["dotenv:build", "dotenv-rails:build"]
task install: ["dotenv:install", "dotenv-rails:install"]
task release: ["dotenv:release", "dotenv-rails:release"]
desc "Run all specs"
RSpec::Core::RakeTask.new(:spec) do |t|
t.rspec_opts = %w[--color]
t.verbose = false
end
Rake::TestTask.new do |t|
t.test_files = Dir["test/**/*_test.rb"]
end
task default: [:spec, :test, :standard]
================================================
FILE: benchmark/parse_ips.rb
================================================
require "bundler/setup"
require "dotenv"
require "benchmark/ips"
require "tempfile"
f = Tempfile.create("benchmark_ips.env")
1000.times.map { |i| f.puts "VAR_#{i}=#{i}" }
f.close
Benchmark.ips do |x|
x.report("parse, overwrite:false") { Dotenv.parse(f.path, overwrite: false) }
x.report("parse, overwrite:true") { Dotenv.parse(f.path, overwrite: true) }
end
File.unlink(f.path)
================================================
FILE: benchmark/parse_profile.rb
================================================
require "bundler/setup"
require "dotenv"
require "stackprof"
require "benchmark/ips"
require "tempfile"
f = Tempfile.create("benchmark_ips.env")
1000.times.map { |i| f.puts "VAR_#{i}=#{i}" }
f.close
profile = StackProf.run(mode: :wall, interval: 1_000) do
10_000.times do
Dotenv.parse(f.path, overwrite: false)
end
end
result = StackProf::Report.new(profile)
puts
result.print_text
puts "\n\n\n"
result.print_method(/Dotenv.parse/)
File.unlink(f.path)
================================================
FILE: bin/dotenv
================================================
#!/usr/bin/env ruby
require "dotenv/cli"
Dotenv::CLI.new(ARGV).run
================================================
FILE: dotenv-rails.gemspec
================================================
require File.expand_path("../lib/dotenv/version", __FILE__)
require "English"
Gem::Specification.new "dotenv-rails", Dotenv::VERSION do |gem|
gem.authors = ["Brandon Keepers"]
gem.email = ["brandon@opensoul.org"]
gem.description = gem.summary = "Autoload dotenv in Rails."
gem.homepage = "https://github.com/bkeepers/dotenv"
gem.license = "MIT"
gem.files = `git ls-files lib | grep dotenv-rails.rb`.split("\n") + ["README.md", "LICENSE"]
gem.add_dependency "dotenv", Dotenv::VERSION
gem.add_dependency "railties", ">= 6.1"
gem.add_development_dependency "spring"
gem.metadata = {
"changelog_uri" => "https://github.com/bkeepers/dotenv/releases",
"funding_uri" => "https://github.com/sponsors/bkeepers"
}
end
================================================
FILE: dotenv.gemspec
================================================
require File.expand_path("../lib/dotenv/version", __FILE__)
require "English"
Gem::Specification.new "dotenv", Dotenv::VERSION do |gem|
gem.authors = ["Brandon Keepers"]
gem.email = ["brandon@opensoul.org"]
gem.description = gem.summary = "Loads environment variables from `.env`."
gem.homepage = "https://github.com/bkeepers/dotenv"
gem.license = "MIT"
gem.files = `git ls-files README.md LICENSE lib bin | grep -v dotenv-rails.rb`.split("\n")
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
gem.add_development_dependency "rake"
gem.add_development_dependency "rspec"
gem.add_development_dependency "standard"
gem.required_ruby_version = ">= 3.0"
gem.metadata = {
"changelog_uri" => "https://github.com/bkeepers/dotenv/releases",
"funding_uri" => "https://github.com/sponsors/bkeepers"
}
end
================================================
FILE: lib/dotenv/autorestore.rb
================================================
# Automatically restore `ENV` to its original state after
if defined?(RSpec.configure)
RSpec.configure do |config|
# Save ENV before the suite starts
config.before(:suite) { Dotenv.save }
# Restore ENV after each example
config.after { Dotenv.restore }
end
end
if defined?(ActiveSupport)
ActiveSupport.on_load(:active_support_test_case) do
ActiveSupport::TestCase.class_eval do
# Save ENV before each test
setup { Dotenv.save }
# Restore ENV after each test
teardown do
Dotenv.restore
rescue ThreadError => e
# Restore will fail if running tests in parallel.
warn e.message
warn "Set `config.dotenv.autorestore = false` in `config/initializers/test.rb`" if defined?(Dotenv::Rails)
end
end
end
end
================================================
FILE: lib/dotenv/cli.rb
================================================
require "dotenv"
require "dotenv/version"
require "dotenv/template"
require "optparse"
module Dotenv
# The `dotenv` command line interface. Run `$ dotenv --help` to see usage.
class CLI < OptionParser
attr_reader :argv, :filenames, :overwrite
def initialize(argv = [])
@argv = argv.dup
@filenames = []
@ignore = false
@overwrite = false
super("Usage: dotenv [options]")
separator ""
on("-f FILES", Array, "List of env files to parse") do |list|
@filenames = list
end
on("-i", "--ignore", "ignore missing env files") do
@ignore = true
end
on("-o", "--overwrite", "overwrite existing ENV variables") do
@overwrite = true
end
on("--overload") { @overwrite = true }
on("-h", "--help", "Display help") do
puts self
exit
end
on("-v", "--version", "Show version") do
puts "dotenv #{Dotenv::VERSION}"
exit
end
on("-t", "--template=FILE", "Create a template env file") do |file|
template = Dotenv::EnvTemplate.new(file)
template.create_template
end
order!(@argv)
end
def run
Dotenv.load(*@filenames, overwrite: @overwrite, ignore: @ignore)
rescue Errno::ENOENT => e
abort e.message
else
exec(*@argv) unless @argv.empty?
end
end
end
================================================
FILE: lib/dotenv/diff.rb
================================================
module Dotenv
# A diff between multiple states of ENV.
class Diff
# The initial state
attr_reader :a
# The final or current state
attr_reader :b
# Create a new diff. If given a block, the state of ENV after the block will be preserved as
# the final state for comparison. Otherwise, the current ENV will be the final state.
#
# @param a [Hash] the initial state, defaults to a snapshot of current ENV
# @param b [Hash] the final state, defaults to the current ENV
# @yield [diff] a block to execute before recording the final state
def initialize(a: snapshot, b: ENV, &block)
@a, @b = a, b
block&.call self
ensure
@b = snapshot if block
end
# Return a Hash of keys added with their new values
def added
b.slice(*(b.keys - a.keys))
end
# Returns a Hash of keys removed with their previous values
def removed
a.slice(*(a.keys - b.keys))
end
# Returns of Hash of keys changed with an array of their previous and new values
def changed
(b.slice(*a.keys).to_a - a.to_a).map do |(k, v)|
[k, [a[k], v]]
end.to_h
end
# Returns a Hash of all added, changed, and removed keys and their new values
def env
b.slice(*(added.keys + changed.keys)).merge(removed.transform_values { |v| nil })
end
# Returns true if any keys were added, removed, or changed
def any?
[added, removed, changed].any?(&:any?)
end
private
def snapshot
# `dup` should not be required here, but some people use `stub_const` to replace ENV with
# a `Hash`. This ensures that we get a frozen copy of that instead of freezing the original.
# https://github.com/bkeepers/dotenv/issues/482
ENV.to_h.dup.freeze
end
end
end
================================================
FILE: lib/dotenv/environment.rb
================================================
module Dotenv
# A `.env` file that will be read and parsed into a Hash
class Environment < Hash
attr_reader :filename, :overwrite
# Create a new Environment
#
# @param filename [String] the path to the file to read
# @param overwrite [Boolean] whether the parser should assume existing values will be overwritten
def initialize(filename, overwrite: false)
super()
@filename = filename
@overwrite = overwrite
load
end
def load
update Parser.call(read, overwrite: overwrite)
end
def read
File.open(@filename, "rb:bom|utf-8", &:read)
end
end
end
================================================
FILE: lib/dotenv/load.rb
================================================
require "dotenv"
defined?(Dotenv::Rails) ? Dotenv::Rails.load : Dotenv.load
================================================
FILE: lib/dotenv/log_subscriber.rb
================================================
require "active_support/log_subscriber"
module Dotenv
# Logs instrumented events
#
# Usage:
# require "active_support/notifications"
# require "dotenv/log_subscriber"
# Dotenv.instrumenter = ActiveSupport::Notifications
#
class LogSubscriber < ActiveSupport::LogSubscriber
attach_to :dotenv
def logger
Dotenv::Rails.logger
end
def load(event)
env = event.payload[:env]
info "Loaded #{color_filename(env.filename)}"
end
def update(event)
diff = event.payload[:diff]
changed = diff.env.keys.map { |key| color_var(key) }
debug "Set #{changed.join(", ")}" if diff.any?
end
def save(event)
info "Saved a snapshot of #{color_env_constant}"
end
def restore(event)
diff = event.payload[:diff]
removed = diff.removed.keys.map { |key| color(key, :RED) }
restored = (diff.changed.keys + diff.added.keys).map { |key| color_var(key) }
if removed.any? || restored.any?
info "Restored snapshot of #{color_env_constant}"
debug "Unset #{removed.join(", ")}" if removed.any?
debug "Restored #{restored.join(", ")}" if restored.any?
end
end
private
def color_filename(filename)
color(Pathname.new(filename).relative_path_from(Dotenv::Rails.root.to_s).to_s, :YELLOW)
end
def color_var(name)
color(name, :CYAN)
end
def color_env_constant
color("ENV", :GREEN)
end
end
end
================================================
FILE: lib/dotenv/missing_keys.rb
================================================
module Dotenv
class Error < StandardError; end
class MissingKeys < Error # :nodoc:
def initialize(keys)
key_word = "key#{"s" if keys.size > 1}"
super("Missing required configuration #{key_word}: #{keys.inspect}")
end
end
end
================================================
FILE: lib/dotenv/parser.rb
================================================
require "dotenv/substitutions/variable"
require "dotenv/substitutions/command" if RUBY_VERSION > "1.8.7"
module Dotenv
# Error raised when encountering a syntax error while parsing a .env file.
class FormatError < SyntaxError; end
# Parses the `.env` file format into key/value pairs.
# It allows for variable substitutions, command substitutions, and exporting of variables.
class Parser
@substitutions = [
Dotenv::Substitutions::Command,
Dotenv::Substitutions::Variable
]
LINE = /
(?:^|\A) # beginning of line
\s* # leading whitespace
(?export\s+)? # optional export
(?[\w.]+) # key
(?: # optional separator and value
(?:\s*=\s*?|:\s+?) # separator
(? # optional value begin
\s*'(?:\\'|[^'])*' # single quoted value
| # or
\s*"(?:\\"|[^"])*" # double quoted value
| # or
[^\#\n]+ # unquoted value
)? # value end
)? # separator and value end
\s* # trailing whitespace
(?:\#.*)? # optional comment
(?:$|\z) # end of line
/x
QUOTED_STRING = /\A(['"])(.*)\1\z/m
class << self
attr_reader :substitutions
def call(...)
new(...).call
end
end
def initialize(string, overwrite: false)
# Convert line breaks to same format
@string = string.gsub(/\r\n?/, "\n")
@hash = {}
@overwrite = overwrite
end
def call
@string.scan(LINE) do
match = $LAST_MATCH_INFO
if existing?(match[:key])
# Use value from already defined variable
@hash[match[:key]] = ENV[match[:key]]
elsif match[:export] && !match[:value]
# Check for exported variable with no value
if !@hash.member?(match[:key])
raise FormatError, "Line #{match.to_s.inspect} has an unset variable"
end
else
@hash[match[:key]] = parse_value(match[:value] || "")
end
end
@hash
end
private
# Determine if a variable is already defined and should not be overwritten.
def existing?(key)
!@overwrite && key != "DOTENV_LINEBREAK_MODE" && ENV.key?(key)
end
def parse_value(value)
# Remove surrounding quotes
value = value.strip.sub(QUOTED_STRING, '\2')
maybe_quote = Regexp.last_match(1)
# Expand new lines in double quoted values
value = expand_newlines(value) if maybe_quote == '"'
# Unescape characters and performs substitutions unless value is single quoted
if maybe_quote != "'"
value = unescape_characters(value)
self.class.substitutions.each { |proc| value = proc.call(value, @hash) }
end
value
end
def unescape_characters(value)
value.gsub(/\\([^$])/, '\1')
end
def expand_newlines(value)
if (@hash["DOTENV_LINEBREAK_MODE"] || ENV["DOTENV_LINEBREAK_MODE"]) == "legacy"
value.gsub('\n', "\n").gsub('\r', "\r")
else
value.gsub('\n', "\\\\\\n").gsub('\r', "\\\\\\r")
end
end
end
end
================================================
FILE: lib/dotenv/rails-now.rb
================================================
# If you use gems that require environment variables to be set before they are
# loaded, then list `dotenv` in the `Gemfile` before those other gems and
# require `dotenv/load`.
#
# gem "dotenv", require: "dotenv/load"
# gem "gem-that-requires-env-variables"
#
require "dotenv/load"
warn '[DEPRECATION] `require "dotenv/rails-now"` is deprecated. Use `require "dotenv/load"` instead.', caller(1..1).first
================================================
FILE: lib/dotenv/rails.rb
================================================
# Since rubygems doesn't support optional dependencies, we have to manually check
unless Gem::Requirement.new(">= 6.1").satisfied_by?(Gem::Version.new(Rails.version))
warn "dotenv 3.0 only supports Rails 6.1 or later. Use dotenv ~> 2.0."
return
end
require "dotenv/replay_logger"
require "dotenv/log_subscriber"
Dotenv.instrumenter = ActiveSupport::Notifications
# Watch all loaded env files with Spring
ActiveSupport::Notifications.subscribe("load.dotenv") do |*args|
if defined?(Spring) && Spring.respond_to?(:watch)
event = ActiveSupport::Notifications::Event.new(*args)
Spring.watch event.payload[:env].filename if Rails.application
end
end
module Dotenv
# Rails integration for using Dotenv to load ENV variables from a file
class Rails < ::Rails::Railtie
delegate :files, :files=, :overwrite, :overwrite=, :autorestore, :autorestore=, :logger, to: "config.dotenv"
def initialize
super
config.dotenv = ActiveSupport::OrderedOptions.new.update(
# Rails.logger is not available yet, so we'll save log messages and replay them when it is
logger: Dotenv::ReplayLogger.new,
overwrite: false,
files: [
".env.#{env}.local",
(".env.local" unless env.test?),
".env.#{env}",
".env"
].compact,
autorestore: env.test? && !defined?(ClimateControl) && !defined?(IceAge)
)
end
# Public: Load dotenv
#
# This will get called during the `before_configuration` callback, but you
# can manually call `Dotenv::Rails.load` if you needed it sooner.
def load
Dotenv.load(*files.map { |file| root.join(file).to_s }, overwrite: overwrite)
end
def overload
deprecator.warn("Dotenv::Rails.overload is deprecated. Set `Dotenv::Rails.overwrite = true` and call Dotenv::Rails.load instead.")
Dotenv.load(*files.map { |file| root.join(file).to_s }, overwrite: true)
end
# Internal: `Rails.root` is nil in Rails 4.1 before the application is
# initialized, so this falls back to the `RAILS_ROOT` environment variable,
# or the current working directory.
def root
::Rails.root || Pathname.new(ENV["RAILS_ROOT"] || Dir.pwd)
end
# Set a new logger and replay logs
def logger=(new_logger)
logger.replay new_logger if logger.is_a?(ReplayLogger)
config.dotenv.logger = new_logger
end
# The current environment that the app is running in.
#
# When running `rake`, the Rails application is initialized in development, so we have to
# check which rake tasks are being run to determine the environment.
#
# See https://github.com/bkeepers/dotenv/issues/219
def env
@env ||= if defined?(Rake.application) && Rake.application.top_level_tasks.grep(TEST_RAKE_TASKS).any?
env = Rake.application.options.show_tasks ? "development" : "test"
ActiveSupport::EnvironmentInquirer.new(env)
else
::Rails.env
end
end
TEST_RAKE_TASKS = /^(default$|test(:|$)|parallel:spec|spec(:|$))/
def deprecator # :nodoc:
@deprecator ||= ActiveSupport::Deprecation.new
end
# Rails uses `#method_missing` to delegate all class methods to the
# instance, which means `Kernel#load` gets called here. We don't want that.
def self.load
instance.load
end
initializer "dotenv", after: :initialize_logger do |app|
if logger.is_a?(ReplayLogger)
self.logger = ActiveSupport::TaggedLogging.new(::Rails.logger).tagged("dotenv")
end
end
initializer "dotenv.deprecator" do |app|
app.deprecators[:dotenv] = deprecator if app.respond_to?(:deprecators)
end
initializer "dotenv.autorestore" do |app|
require "dotenv/autorestore" if autorestore
end
config.before_configuration { load }
end
Railtie = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Dotenv::Railtie", "Dotenv::Rails", Dotenv::Rails.deprecator)
end
================================================
FILE: lib/dotenv/replay_logger.rb
================================================
module Dotenv
# A logger that can be used before the apps real logger is initialized.
class ReplayLogger < Logger
def initialize
super(nil) # Doesn't matter what this is, it won't be used.
@logs = []
end
# Override the add method to store logs so we can replay them to a real logger later.
def add(*args, &block)
@logs.push([args, block])
end
# Replay the store logs to a real logger.
def replay(logger)
@logs.each { |args, block| logger.add(*args, &block) }
@logs.clear
end
end
end
================================================
FILE: lib/dotenv/substitutions/command.rb
================================================
require "English"
module Dotenv
module Substitutions
# Substitute shell commands in a value.
#
# SHA=$(git rev-parse HEAD)
#
module Command
class << self
INTERPOLATED_SHELL_COMMAND = /
(?\\)? # is it escaped with a backslash?
\$ # literal $
(? # collect command content for eval
\( # require opening paren
(?:[^()]|\g)+ # allow any number of non-parens, or balanced
# parens (by nesting the expression
# recursively)
\) # require closing paren
)
/x
def call(value, env)
# Process interpolated shell commands
value.gsub(INTERPOLATED_SHELL_COMMAND) do |*|
# Eliminate opening and closing parentheses
command = $LAST_MATCH_INFO[:cmd][1..-2]
if $LAST_MATCH_INFO[:backslash]
# Command is escaped, don't replace it.
$LAST_MATCH_INFO[0][1..]
else
# Execute the command and return the value
`#{Variable.call(command, env)}`.chomp
end
end
end
end
end
end
end
================================================
FILE: lib/dotenv/substitutions/variable.rb
================================================
require "English"
module Dotenv
module Substitutions
# Substitute variables in a value.
#
# HOST=example.com
# URL="https://$HOST"
#
module Variable
class << self
VARIABLE = /
(\\)? # is it escaped with a backslash?
(\$) # literal $
(?!\() # shouldn't be followed by parenthesis
\{? # allow brace wrapping
([A-Z0-9_]+)? # optional alpha nums
\}? # closing brace
/xi
def call(value, env)
value.gsub(VARIABLE) do |variable|
match = $LAST_MATCH_INFO
if match[1] == "\\"
variable[1..]
elsif match[3]
env[match[3]] || ENV[match[3]] || ""
else
variable
end
end
end
end
end
end
end
================================================
FILE: lib/dotenv/tasks.rb
================================================
desc "Load environment settings from .env"
task :dotenv do
require "dotenv"
Dotenv.load
end
task environment: :dotenv
================================================
FILE: lib/dotenv/template.rb
================================================
module Dotenv
EXPORT_COMMAND = "export ".freeze
# Class for creating a template from a env file
class EnvTemplate
def initialize(env_file)
@env_file = env_file
end
def create_template
File.open(@env_file, "r") do |env_file|
File.open("#{@env_file}.template", "w") do |env_template|
env_file.each do |line|
if is_comment?(line)
env_template.puts line
elsif (var = var_defined?(line))
if line.match(EXPORT_COMMAND)
env_template.puts "export #{var}=#{var}"
else
env_template.puts "#{var}=#{var}"
end
elsif line_blank?(line)
env_template.puts
end
end
end
end
end
private
def is_comment?(line)
line.strip.start_with?("#")
end
def var_defined?(line)
match = Dotenv::Parser::LINE.match(line)
match && match[:key]
end
def line_blank?(line)
line.strip.length.zero?
end
end
end
================================================
FILE: lib/dotenv/version.rb
================================================
module Dotenv
VERSION = "3.2.0".freeze
end
================================================
FILE: lib/dotenv-rails.rb
================================================
require "dotenv"
================================================
FILE: lib/dotenv.rb
================================================
require "dotenv/version"
require "dotenv/parser"
require "dotenv/environment"
require "dotenv/missing_keys"
require "dotenv/diff"
# Shim to load environment variables from `.env files into `ENV`.
module Dotenv
extend self
# An internal monitor to synchronize access to ENV in multi-threaded environments.
SEMAPHORE = Monitor.new
private_constant :SEMAPHORE
attr_accessor :instrumenter
# Loads environment variables from one or more `.env` files. See `#parse` for more details.
def load(*filenames, overwrite: false, ignore: true)
parse(*filenames, overwrite: overwrite, ignore: ignore) do |env|
instrument(:load, env: env) do |payload|
update(env, overwrite: overwrite)
end
end
end
# Same as `#load`, but raises Errno::ENOENT if any files don't exist
def load!(*filenames)
load(*filenames, ignore: false)
end
# same as `#load`, but will overwrite existing values in `ENV`
def overwrite(*filenames)
load(*filenames, overwrite: true)
end
alias_method :overload, :overwrite
# same as `#overwrite`, but raises Errno::ENOENT if any files don't exist
def overwrite!(*filenames)
load(*filenames, overwrite: true, ignore: false)
end
alias_method :overload!, :overwrite!
# Parses the given files, yielding for each file if a block is given.
#
# @param filenames [String, Array] Files to parse
# @param overwrite [Boolean] Overwrite existing `ENV` values
# @param ignore [Boolean] Ignore non-existent files
# @param block [Proc] Block to yield for each parsed `Dotenv::Environment`
# @return [Hash] parsed key/value pairs
def parse(*filenames, overwrite: false, ignore: true, &block)
filenames << ".env" if filenames.empty?
filenames = filenames.reverse if overwrite
filenames.reduce({}) do |hash, filename|
begin
env = Environment.new(File.expand_path(filename), overwrite: overwrite)
env = block.call(env) if block
rescue Errno::ENOENT, Errno::EISDIR
raise unless ignore
end
hash.merge! env || {}
end
end
# Save the current `ENV` to be restored later
def save
instrument(:save) do |payload|
@diff = payload[:diff] = Dotenv::Diff.new
end
end
# Restore `ENV` to a given state
#
# @param env [Hash] Hash of keys and values to restore, defaults to the last saved state
# @param safe [Boolean] Is it safe to modify `ENV`? Defaults to `true` in the main thread, otherwise raises an error.
def restore(env = @diff&.a, safe: Thread.current == Thread.main)
# No previously saved or provided state to restore
return unless env
diff = Dotenv::Diff.new(b: env)
return unless diff.any?
unless safe
raise ThreadError, <<~EOE.tr("\n", " ")
Dotenv.restore is not thread safe. Use `Dotenv.modify { }` to update ENV for the duration
of the block in a thread safe manner, or call `Dotenv.restore(safe: true)` to ignore
this error.
EOE
end
instrument(:restore, diff: diff) { ENV.replace(env) }
end
# Update `ENV` with the given hash of keys and values
#
# @param env [Hash] Hash of keys and values to set in `ENV`
# @param overwrite [Boolean|:warn] Overwrite existing `ENV` values
def update(env = {}, overwrite: false)
instrument(:update) do |payload|
diff = payload[:diff] = Dotenv::Diff.new do
ENV.update(env.transform_keys(&:to_s)) do |key, old_value, new_value|
# This block is called when a key exists. Return the new value if overwrite is true.
case overwrite
when :warn
# not printing the value since that could be a secret
warn "Warning: dotenv not overwriting ENV[#{key.inspect}]"
old_value
when true then new_value
when false then old_value
else raise ArgumentError, "Invalid value for overwrite: #{overwrite.inspect}"
end
end
end
diff.env
end
end
# Modify `ENV` for the block and restore it to its previous state afterwards.
#
# Note that the block is synchronized to prevent concurrent modifications to `ENV`,
# so multiple threads will be executed serially.
#
# @param env [Hash] Hash of keys and values to set in `ENV`
def modify(env = {}, &block)
SEMAPHORE.synchronize do
diff = Dotenv::Diff.new
update(env, overwrite: true)
block.call
ensure
restore(diff.a, safe: true)
end
end
def require_keys(*keys)
missing_keys = keys.flatten - ::ENV.keys
return if missing_keys.empty?
raise MissingKeys, missing_keys
end
private
def instrument(name, payload = {}, &block)
if instrumenter
instrumenter.instrument("#{name}.dotenv", payload, &block)
else
block&.call payload
end
end
end
require "dotenv/rails" if defined?(Rails::Railtie)
================================================
FILE: spec/dotenv/cli_spec.rb
================================================
require "spec_helper"
require "dotenv/cli"
describe "dotenv binary" do
before do
Dir.chdir(File.expand_path("../../fixtures", __FILE__))
end
def run(*args)
Dotenv::CLI.new(args).run
end
it "loads from .env by default" do
expect(ENV).not_to have_key("DOTENV")
run
expect(ENV).to have_key("DOTENV")
end
it "loads from file specified by -f" do
expect(ENV).not_to have_key("OPTION_A")
run "-f", "plain.env"
expect(ENV).to have_key("OPTION_A")
end
it "dies if file specified by -f doesn't exist" do
expect do
capture_output { run "-f", ".doesnotexist" }
end.to raise_error(SystemExit, /No such file/)
end
it "ignores missing files when --ignore flag given" do
expect do
run "--ignore", "-f", ".doesnotexist"
end.not_to raise_error
end
it "loads from multiple files specified by -f" do
expect(ENV).not_to have_key("PLAIN")
expect(ENV).not_to have_key("QUOTED")
run "-f", "plain.env,quoted.env"
expect(ENV).to have_key("PLAIN")
expect(ENV).to have_key("QUOTED")
end
it "does not consume non-dotenv flags by accident" do
cli = Dotenv::CLI.new(["-f", "plain.env", "foo", "--switch"])
expect(cli.filenames).to eql(["plain.env"])
expect(cli.argv).to eql(["foo", "--switch"])
end
it "does not consume dotenv flags from subcommand" do
cli = Dotenv::CLI.new(["foo", "-f", "something"])
expect(cli.filenames).to eql([])
expect(cli.argv).to eql(["foo", "-f", "something"])
end
it "does not mess with quoted args" do
cli = Dotenv::CLI.new(["foo something"])
expect(cli.filenames).to eql([])
expect(cli.argv).to eql(["foo something"])
end
describe "templates a file specified by -t" do
before do
@buffer = StringIO.new
@origin_filename = "plain.env"
@template_filename = "plain.env.template"
end
it "templates variables" do
@input = StringIO.new("FOO=BAR\nFOO2=BAR2")
allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
# call the function that writes to the file
Dotenv::CLI.new(["-t", @origin_filename])
# reading the buffer and checking its content.
expect(@buffer.string).to eq("FOO=FOO\nFOO2=FOO2\n")
end
it "templates variables with export prefix" do
@input = StringIO.new("export FOO=BAR\nexport FOO2=BAR2")
allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
Dotenv::CLI.new(["-t", @origin_filename])
expect(@buffer.string).to eq("export FOO=FOO\nexport FOO2=FOO2\n")
end
it "templates multi-line variables" do
@input = StringIO.new(<<~TEXT)
FOO=BAR
FOO2="BAR2
BAR2"
TEXT
allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
# call the function that writes to the file
Dotenv::CLI.new(["-t", @origin_filename])
# reading the buffer and checking its content.
expect(@buffer.string).to eq("FOO=FOO\nFOO2=FOO2\n")
end
it "ignores blank lines" do
@input = StringIO.new("\nFOO=BAR\nFOO2=BAR2")
allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@input)
allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
Dotenv::CLI.new(["-t", @origin_filename])
expect(@buffer.string).to eq("\nFOO=FOO\nFOO2=FOO2\n")
end
it "ignores comments" do
@comment_input = StringIO.new("#Heading comment\nFOO=BAR\nFOO2=BAR2\n")
allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_input)
allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
Dotenv::CLI.new(["-t", @origin_filename])
expect(@buffer.string).to eq("#Heading comment\nFOO=FOO\nFOO2=FOO2\n")
end
it "ignores comments with =" do
@comment_with_equal_input = StringIO.new("#Heading=comment\nFOO=BAR\nFOO2=BAR2")
allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_with_equal_input)
allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
Dotenv::CLI.new(["-t", @origin_filename])
expect(@buffer.string).to eq("#Heading=comment\nFOO=FOO\nFOO2=FOO2\n")
end
it "ignores comments with leading spaces" do
@comment_leading_spaces_input = StringIO.new(" #Heading comment\nFOO=BAR\nFOO2=BAR2")
allow(File).to receive(:open).with(@origin_filename, "r").and_yield(@comment_leading_spaces_input)
allow(File).to receive(:open).with(@template_filename, "w").and_yield(@buffer)
Dotenv::CLI.new(["-t", @origin_filename])
expect(@buffer.string).to eq(" #Heading comment\nFOO=FOO\nFOO2=FOO2\n")
end
end
end
================================================
FILE: spec/dotenv/diff_spec.rb
================================================
require "spec_helper"
describe Dotenv::Diff do
let(:before) { {} }
let(:after) { {} }
subject { Dotenv::Diff.new(a: before, b: after) }
context "no changes" do
let(:before) { {"A" => 1} }
let(:after) { {"A" => 1} }
it { expect(subject.added).to eq({}) }
it { expect(subject.removed).to eq({}) }
it { expect(subject.changed).to eq({}) }
it { expect(subject.any?).to eq(false) }
it { expect(subject.env).to eq({}) }
end
context "key added" do
let(:after) { {"A" => 1} }
it { expect(subject.added).to eq("A" => 1) }
it { expect(subject.removed).to eq({}) }
it { expect(subject.changed).to eq({}) }
it { expect(subject.any?).to eq(true) }
it { expect(subject.env).to eq("A" => 1) }
end
context "key removed" do
let(:before) { {"A" => 1} }
it { expect(subject.added).to eq({}) }
it { expect(subject.removed).to eq("A" => 1) }
it { expect(subject.changed).to eq({}) }
it { expect(subject.any?).to eq(true) }
it { expect(subject.env).to eq("A" => nil) }
end
context "key changed" do
let(:before) { {"A" => 1} }
let(:after) { {"A" => 2} }
it { expect(subject.added).to eq({}) }
it { expect(subject.removed).to eq({}) }
it { expect(subject.changed).to eq("A" => [1, 2]) }
it { expect(subject.any?).to eq(true) }
it { expect(subject.env).to eq("A" => 2) }
end
end
================================================
FILE: spec/dotenv/environment_spec.rb
================================================
require "spec_helper"
describe Dotenv::Environment do
subject { env("OPTION_A=1\nOPTION_B=2") }
describe "initialize" do
it "reads the file" do
expect(subject["OPTION_A"]).to eq("1")
expect(subject["OPTION_B"]).to eq("2")
end
it "fails if file does not exist" do
expect do
Dotenv::Environment.new(".does_not_exists")
end.to raise_error(Errno::ENOENT)
end
end
require "tempfile"
def env(text, ...)
file = Tempfile.new("dotenv")
file.write text
file.close
env = Dotenv::Environment.new(file.path, ...)
file.unlink
env
end
end
================================================
FILE: spec/dotenv/log_subscriber_spec.rb
================================================
require "spec_helper"
require "active_support/all"
require "rails"
require "dotenv/rails"
describe Dotenv::LogSubscriber do
let(:logs) { StringIO.new }
before do
Dotenv.instrumenter = ActiveSupport::Notifications
Dotenv::Rails.logger = Logger.new(logs)
end
describe "load" do
it "logs when a file is loaded" do
Dotenv.load(fixture_path("plain.env"))
expect(logs.string).to match(/Loaded.*plain.env/)
expect(logs.string).to match(/Set.*PLAIN/)
end
end
context "update" do
it "logs when a new instance variable is set" do
Dotenv.update({"PLAIN" => "true"})
expect(logs.string).to match(/Set.*PLAIN/)
end
it "logs when an instance variable is overwritten" do
ENV["PLAIN"] = "nope"
Dotenv.update({"PLAIN" => "true"}, overwrite: true)
expect(logs.string).to match(/Set.*PLAIN/)
end
it "does not log when an instance variable is not overwritten" do
ENV["FOO"] = "existing"
Dotenv.update({"FOO" => "new"})
expect(logs.string).not_to match(/FOO/)
end
it "does not log when an instance variable is unchanged" do
ENV["PLAIN"] = "true"
Dotenv.update({"PLAIN" => "true"}, overwrite: true)
expect(logs.string).not_to match(/PLAIN/)
end
end
context "save" do
it "logs when a snapshot is saved" do
Dotenv.save
expect(logs.string).to match(/Saved/)
end
end
context "restore" do
it "logs restored keys" do
previous_value = ENV["PWD"]
ENV["PWD"] = "/tmp"
Dotenv.restore
expect(logs.string).to match(/Restored.*PWD/)
# Does not log value
expect(logs.string).not_to include(previous_value)
end
it "logs unset keys" do
ENV["DOTENV_TEST"] = "LogSubscriber"
Dotenv.restore
expect(logs.string).to match(/Unset.*DOTENV_TEST/)
end
it "does not log if no keys unset or restored" do
Dotenv.restore
expect(logs.string).not_to match(/Restored|Unset/)
end
end
end
================================================
FILE: spec/dotenv/parser_spec.rb
================================================
require "spec_helper"
describe Dotenv::Parser do
def env(...)
Dotenv::Parser.call(...)
end
it "parses unquoted values" do
expect(env("FOO=bar")).to eql("FOO" => "bar")
end
it "parses unquoted values with spaces after seperator" do
expect(env("FOO= bar")).to eql("FOO" => "bar")
end
it "parses unquoted escape characters correctly" do
expect(env("FOO=bar\\ bar")).to eql("FOO" => "bar bar")
end
it "parses values with spaces around equal sign" do
expect(env("FOO =bar")).to eql("FOO" => "bar")
expect(env("FOO= bar")).to eql("FOO" => "bar")
end
it "parses values with leading spaces" do
expect(env(" FOO=bar")).to eql("FOO" => "bar")
end
it "parses values with following spaces" do
expect(env("FOO=bar ")).to eql("FOO" => "bar")
end
it "parses double quoted values" do
expect(env('FOO="bar"')).to eql("FOO" => "bar")
end
it "parses double quoted values with following spaces" do
expect(env('FOO="bar" ')).to eql("FOO" => "bar")
end
it "parses single quoted values" do
expect(env("FOO='bar'")).to eql("FOO" => "bar")
end
it "parses single quoted values with following spaces" do
expect(env("FOO='bar' ")).to eql("FOO" => "bar")
end
it "parses escaped double quotes" do
expect(env('FOO="escaped\"bar"')).to eql("FOO" => 'escaped"bar')
end
it "parses empty values" do
expect(env("FOO=")).to eql("FOO" => "")
end
it "expands variables found in values" do
expect(env("FOO=test\nBAR=$FOO")).to eql("FOO" => "test", "BAR" => "test")
end
it "parses variables wrapped in brackets" do
expect(env("FOO=test\nBAR=${FOO}bar"))
.to eql("FOO" => "test", "BAR" => "testbar")
end
it "expands variables from ENV if not found in .env" do
ENV["FOO"] = "test"
expect(env("BAR=$FOO")).to eql("BAR" => "test")
end
it "expands variables from ENV if found in .env during load" do
ENV["FOO"] = "test"
expect(env("FOO=development\nBAR=${FOO}")["BAR"])
.to eql("test")
end
it "doesn't expand variables from ENV if in local env in overwrite" do
ENV["FOO"] = "test"
expect(env("FOO=development\nBAR=${FOO}")["BAR"])
.to eql("test")
end
it "expands undefined variables to an empty string" do
expect(env("BAR=$FOO")).to eql("BAR" => "")
end
it "expands variables in double quoted strings" do
expect(env("FOO=test\nBAR=\"quote $FOO\""))
.to eql("FOO" => "test", "BAR" => "quote test")
end
it "does not expand variables in single quoted strings" do
expect(env("BAR='quote $FOO'")).to eql("BAR" => "quote $FOO")
end
it "does not expand escaped variables" do
expect(env('FOO="foo\$BAR"')).to eql("FOO" => "foo$BAR")
expect(env('FOO="foo\${BAR}"')).to eql("FOO" => "foo${BAR}")
expect(env("FOO=test\nBAR=\"foo\\${FOO} ${FOO}\""))
.to eql("FOO" => "test", "BAR" => "foo${FOO} test")
end
it "parses yaml style options" do
expect(env("OPTION_A: 1")).to eql("OPTION_A" => "1")
end
it "parses export keyword" do
expect(env("export OPTION_A=2")).to eql("OPTION_A" => "2")
end
it "allows export line if you want to do it that way" do
expect(env('OPTION_A=2
export OPTION_A')).to eql("OPTION_A" => "2")
end
it "allows export line if you want to do it that way and checks for unset variables" do
expect do
env('OPTION_A=2
export OH_NO_NOT_SET')
end.to raise_error(Dotenv::FormatError, 'Line "export OH_NO_NOT_SET" has an unset variable')
end
it 'escapes \n in quoted strings' do
expect(env('FOO="bar\nbaz"')).to eql("FOO" => "bar\\nbaz")
expect(env('FOO="bar\\nbaz"')).to eql("FOO" => "bar\\nbaz")
end
it 'expands \n and \r in quoted strings with DOTENV_LINEBREAK_MODE=legacy in current file' do
ENV["DOTENV_LINEBREAK_MODE"] = "strict"
contents = [
"DOTENV_LINEBREAK_MODE=legacy",
'FOO="bar\nbaz\rfizz"'
].join("\n")
expect(env(contents)).to eql("DOTENV_LINEBREAK_MODE" => "legacy", "FOO" => "bar\nbaz\rfizz")
end
it 'expands \n and \r in quoted strings with DOTENV_LINEBREAK_MODE=legacy in ENV' do
ENV["DOTENV_LINEBREAK_MODE"] = "legacy"
contents = 'FOO="bar\nbaz\rfizz"'
expect(env(contents)).to eql("FOO" => "bar\nbaz\rfizz")
end
it 'parses variables with "." in the name' do
expect(env("FOO.BAR=foobar")).to eql("FOO.BAR" => "foobar")
end
it "strips unquoted values" do
expect(env("foo=bar ")).to eql("foo" => "bar") # not 'bar '
end
it "ignores lines that are not variable assignments" do
expect(env("lol$wut")).to eql({})
end
it "ignores empty lines" do
expect(env("\n \t \nfoo=bar\n \nfizz=buzz"))
.to eql("foo" => "bar", "fizz" => "buzz")
end
it "does not ignore empty lines in quoted string" do
value = "a\n\nb\n\nc"
expect(env("FOO=\"#{value}\"")).to eql("FOO" => value)
end
it "ignores inline comments" do
expect(env("foo=bar # this is foo")).to eql("foo" => "bar")
end
it "allows # in quoted value" do
expect(env('foo="bar#baz" # comment')).to eql("foo" => "bar#baz")
end
it "allows # in quoted value with spaces after seperator" do
expect(env('foo= "bar#baz" # comment')).to eql("foo" => "bar#baz")
end
it "ignores comment lines" do
expect(env("\n\n\n # HERE GOES FOO \nfoo=bar")).to eql("foo" => "bar")
end
it "ignores commented out variables" do
expect(env("# HELLO=world\n")).to eql({})
end
it "ignores comment" do
expect(env("# Uncomment to activate:\n")).to eql({})
end
it "includes variables without values" do
input = 'DATABASE_PASSWORD=
DATABASE_USERNAME=root
DATABASE_HOST=/tmp/mysql.sock'
output = {
"DATABASE_PASSWORD" => "",
"DATABASE_USERNAME" => "root",
"DATABASE_HOST" => "/tmp/mysql.sock"
}
expect(env(input)).to eql(output)
end
it "parses # in quoted values" do
expect(env('foo="ba#r"')).to eql("foo" => "ba#r")
expect(env("foo='ba#r'")).to eql("foo" => "ba#r")
end
it "parses # in quoted values with following spaces" do
expect(env('foo="ba#r" ')).to eql("foo" => "ba#r")
expect(env("foo='ba#r' ")).to eql("foo" => "ba#r")
end
it "parses empty values" do
expect(env("foo=")).to eql("foo" => "")
end
it "allows multi-line values in single quotes" do
env_file = %(OPTION_A=first line
export OPTION_B='line 1
line 2
line 3'
OPTION_C="last line"
OPTION_ESCAPED='line one
this is \\'quoted\\'
one more line')
expected_result = {
"OPTION_A" => "first line",
"OPTION_B" => "line 1\nline 2\nline 3",
"OPTION_C" => "last line",
"OPTION_ESCAPED" => "line one\nthis is \\'quoted\\'\none more line"
}
expect(env(env_file)).to eql(expected_result)
end
it "allows multi-line values in double quotes" do
env_file = %(OPTION_A=first line
export OPTION_B="line 1
line 2
line 3"
OPTION_C="last line"
OPTION_ESCAPED="line one
this is \\"quoted\\"
one more line")
expected_result = {
"OPTION_A" => "first line",
"OPTION_B" => "line 1\nline 2\nline 3",
"OPTION_C" => "last line",
"OPTION_ESCAPED" => "line one\nthis is \"quoted\"\none more line"
}
expect(env(env_file)).to eql(expected_result)
end
if RUBY_VERSION > "1.8.7"
it "parses shell commands interpolated in $()" do
expect(env("echo=$(echo hello)")).to eql("echo" => "hello")
end
it "allows balanced parentheses within interpolated shell commands" do
expect(env('echo=$(echo "$(echo "$(echo "$(echo hello)")")")'))
.to eql("echo" => "hello")
end
it "doesn't interpolate shell commands when escape says not to" do
expect(env('echo=escaped-\$(echo hello)'))
.to eql("echo" => "escaped-$(echo hello)")
end
it "is not thrown off by quotes in interpolated shell commands" do
expect(env('interp=$(echo "Quotes won\'t be a problem")')["interp"])
.to eql("Quotes won't be a problem")
end
it "handles parentheses in variables in commands" do
expect(env("FOO='passwo(rd'\nBAR=$(echo '$FOO')")).to eql("FOO" => "passwo(rd", "BAR" => "passwo(rd")
end
it "handles command to variable to command chain" do
expect(env("FOO=$(echo bar)\nBAR=$(echo $FOO)")).to eql("FOO" => "bar", "BAR" => "bar")
end
it "supports carriage return" do
expect(env("FOO=bar\rbaz=fbb")).to eql("FOO" => "bar", "baz" => "fbb")
end
it "supports carriage return combine with new line" do
expect(env("FOO=bar\r\nbaz=fbb")).to eql("FOO" => "bar", "baz" => "fbb")
end
it "escapes carriage return in quoted strings" do
expect(env('FOO="bar\rbaz"')).to eql("FOO" => "bar\\rbaz")
end
it "escape $ properly when no alphabets/numbers/_ are followed by it" do
expect(env("FOO=\"bar\\$ \\$\\$\"")).to eql("FOO" => "bar$ $$")
end
# echo bar $ -> prints bar $ in the shell
it "ignore $ when it is not escaped and no variable is followed by it" do
expect(env("FOO=\"bar $ \"")).to eql("FOO" => "bar $ ")
end
# This functionality is not supported on JRuby or Rubinius
if (!defined?(RUBY_ENGINE) || RUBY_ENGINE != "jruby") &&
!defined?(Rubinius)
it "substitutes shell variables within interpolated shell commands" do
expect(env(%(VAR1=var1\ninterp=$(echo "VAR1 is $VAR1")))["interp"])
.to eql("VAR1 is var1")
end
end
end
it "returns existing value for redefined variable" do
ENV["FOO"] = "existing"
expect(env("FOO=bar")).to eql("FOO" => "existing")
end
end
================================================
FILE: spec/dotenv/rails_spec.rb
================================================
require "spec_helper"
require "rails"
require "dotenv/rails"
describe Dotenv::Rails do
let(:log_output) { StringIO.new }
let(:application) do
log_output = self.log_output
Class.new(Rails::Application) do
config.load_defaults Rails::VERSION::STRING.to_f
config.eager_load = false
config.logger = ActiveSupport::Logger.new(log_output)
config.root = fixture_path
# Remove method fails since app is reloaded for each test
config.active_support.remove_deprecated_time_with_zone_name = false
end.instance
end
around do |example|
# These get frozen after the app initializes
autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
# Run in fixtures directory
Dir.chdir(fixture_path) { example.run }
ensure
# Restore autoload paths to unfrozen state
ActiveSupport::Dependencies.autoload_paths = autoload_paths
ActiveSupport::Dependencies.autoload_once_paths = autoload_once_paths
end
before do
Rails.env = "test"
Rails.application = nil
Rails.logger = nil
begin
# Remove the singleton instance if it exists
Dotenv::Rails.remove_instance_variable(:@instance)
rescue
nil
end
end
describe "files" do
it "loads files for development environment" do
Rails.env = "development"
expect(Dotenv::Rails.files).to eql(
[
".env.development.local",
".env.local",
".env.development",
".env"
]
)
end
it "does not load .env.local in test rails environment" do
Rails.env = "test"
expect(Dotenv::Rails.files).to eql(
[
".env.test.local",
".env.test",
".env"
]
)
end
it "can be modified in place" do
Dotenv::Rails.files << ".env.shared"
expect(Dotenv::Rails.files.last).to eq(".env.shared")
end
end
it "watches other loaded files with Spring" do
stub_spring(load_watcher: true)
application.initialize!
path = fixture_path("plain.env")
Dotenv.load(path)
expect(Spring.watcher).to include(path.to_s)
end
it "doesn't raise an error if Spring.watch is not defined" do
stub_spring(load_watcher: false)
expect {
application.initialize!
}.to_not raise_error
end
context "before_configuration" do
it "calls #load" do
expect(Dotenv::Rails.instance).to receive(:load)
ActiveSupport.run_load_hooks(:before_configuration)
end
end
context "load" do
subject { application.initialize! }
it "watches .env with Spring" do
stub_spring(load_watcher: true)
subject
expect(Spring.watcher).to include(fixture_path(".env").to_s)
end
it "loads .env.test before .env" do
subject
expect(ENV["DOTENV"]).to eql("test")
end
it "loads configured files" do
Dotenv::Rails.files = [fixture_path("plain.env")]
expect { subject }.to change { ENV["PLAIN"] }.from(nil).to("true")
end
it "loads file relative to Rails.root" do
allow(Rails).to receive(:root).and_return(Pathname.new("/tmp"))
Dotenv::Rails.files = [".env"]
expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false})
subject
end
it "returns absolute paths unchanged" do
Dotenv::Rails.files = ["/tmp/.env"]
expect(Dotenv).to receive(:load).with("/tmp/.env", {overwrite: false})
subject
end
context "with overwrite = true" do
before { Dotenv::Rails.overwrite = true }
it "overwrites .env with .env.test" do
subject
expect(ENV["DOTENV"]).to eql("test")
end
it "overwrites any existing ENV variables" do
ENV["DOTENV"] = "predefined"
expect { subject }.to(change { ENV["DOTENV"] }.from("predefined").to("test"))
end
end
end
describe "root" do
it "returns Rails.root" do
expect(Dotenv::Rails.root).to eql(Rails.root)
end
context "when Rails.root is nil" do
before do
allow(Rails).to receive(:root).and_return(nil)
end
it "falls back to RAILS_ROOT" do
ENV["RAILS_ROOT"] = "/tmp"
expect(Dotenv::Rails.root.to_s).to eql("/tmp")
end
end
end
describe "autorestore" do
it "is loaded if RAILS_ENV=test" do
expect(Dotenv::Rails.autorestore).to eq(true)
expect(Dotenv::Rails.instance).to receive(:require).with("dotenv/autorestore")
application.initialize!
end
it "is not loaded if RAILS_ENV=development" do
Rails.env = "development"
expect(Dotenv::Rails.autorestore).to eq(false)
expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
application.initialize!
end
it "is not loaded if autorestore set to false" do
Dotenv::Rails.autorestore = false
expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
application.initialize!
end
it "is not loaded if ClimateControl is defined" do
stub_const("ClimateControl", Module.new)
expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
application.initialize!
end
it "is not loaded if IceAge is defined" do
stub_const("IceAge", Module.new)
expect(Dotenv::Rails.instance).not_to receive(:require).with("dotenv/autorestore")
application.initialize!
end
end
describe "logger" do
it "replays to Rails.logger" do
expect(Dotenv::Rails.logger).to be_a(Dotenv::ReplayLogger)
Dotenv::Rails.logger.debug("test")
application.initialize!
expect(Dotenv::Rails.logger).not_to be_a(Dotenv::ReplayLogger)
expect(log_output.string).to include("[dotenv] test")
end
it "does not replace custom logger" do
logger = Logger.new(log_output)
Dotenv::Rails.logger = logger
application.initialize!
expect(Dotenv::Rails.logger).to be(logger)
end
end
def stub_spring(load_watcher: true)
spring = Module.new do
if load_watcher
def self.watcher
@watcher ||= Set.new
end
def self.watch(path)
watcher.add path
end
end
end
stub_const "Spring", spring
end
end
================================================
FILE: spec/dotenv_spec.rb
================================================
require "spec_helper"
describe Dotenv do
before do
Dir.chdir(File.expand_path("../fixtures", __FILE__))
end
shared_examples "load" do
context "with no args" do
let(:env_files) { [] }
it "defaults to .env" do
expect(Dotenv::Environment).to receive(:new).with(expand(".env"), anything).and_call_original
subject
end
end
context "with a tilde path" do
let(:env_files) { ["~/.env"] }
it "expands the path" do
expected = expand("~/.env")
allow(File).to receive(:exist?) { |arg| arg == expected }
expect(Dotenv::Environment).to receive(:new).with(expected, anything)
.and_return(Dotenv::Environment.new(".env"))
subject
end
end
context "with multiple files" do
let(:env_files) { [".env", fixture_path("plain.env")] }
let(:expected) do
{"OPTION_A" => "1",
"OPTION_B" => "2",
"OPTION_C" => "3",
"OPTION_D" => "4",
"OPTION_E" => "5",
"PLAIN" => "true",
"DOTENV" => "true"}
end
it "loads all files" do
subject
expected.each do |key, value|
expect(ENV[key]).to eq(value)
end
end
it "returns hash of loaded variables" do
expect(subject).to eq(expected)
end
it "does not return unchanged variables" do
ENV["OPTION_A"] = "1"
expect(subject).not_to have_key("OPTION_A")
end
end
end
shared_examples "overwrite" do
it_behaves_like "load"
context "with multiple files" do
let(:env_files) { [fixture_path("important.env"), fixture_path("plain.env")] }
let(:expected) do
{
"OPTION_A" => "abc",
"OPTION_B" => "2",
"OPTION_C" => "3",
"OPTION_D" => "4",
"OPTION_E" => "5",
"PLAIN" => "false"
}
end
it "respects the file importance order" do
subject
expected.each do |key, value|
expect(ENV[key]).to eq(value)
end
end
end
end
describe "load" do
let(:env_files) { [] }
let(:options) { {} }
subject { Dotenv.load(*env_files, **options) }
it_behaves_like "load"
it "initializes the Environment with overwrite: false" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
.and_call_original
subject
end
it "warns about not overwriting when requested" do
options[:overwrite] = :warn
ENV["DOTENV"] = "false"
expect(capture_output { subject }).to eq("Warning: dotenv not overwriting ENV[\"DOTENV\"]\n")
expect(ENV["DOTENV"]).to eq("false")
end
context "when the file does not exist" do
let(:env_files) { [".env_does_not_exist"] }
it "fails silently" do
expect { subject }.not_to raise_error
end
it "does not change ENV" do
expect { subject }.not_to change { ENV.inspect }
end
end
context "when the file is a directory" do
let(:env_files) { [] }
around do |example|
Dir.mktmpdir do |dir|
env_files.push dir
example.run
end
end
it "fails silently with ignore: true (default)" do
expect { subject }.not_to raise_error
end
it "raises error with ignore: false" do
options[:ignore] = false
expect { subject }.to raise_error(/Is a directory/)
end
end
end
describe "load!" do
let(:env_files) { [] }
subject { Dotenv.load!(*env_files) }
it_behaves_like "load"
it "initializes Environment with overwrite: false" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
.and_call_original
subject
end
context "when one file exists and one does not" do
let(:env_files) { [".env", ".env_does_not_exist"] }
it "raises an Errno::ENOENT error" do
expect { subject }.to raise_error(Errno::ENOENT)
end
end
end
describe "overwrite" do
let(:env_files) { [fixture_path("plain.env")] }
subject { Dotenv.overwrite(*env_files) }
it_behaves_like "load"
it_behaves_like "overwrite"
it "initializes the Environment overwrite: true" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true)
.and_call_original
subject
end
context "when loading a file containing already set variables" do
let(:env_files) { [fixture_path("plain.env")] }
it "overwrites any existing ENV variables" do
ENV["OPTION_A"] = "predefined"
subject
expect(ENV["OPTION_A"]).to eq("1")
end
end
context "when the file does not exist" do
let(:env_files) { [".env_does_not_exist"] }
it "fails silently" do
expect { subject }.not_to raise_error
end
it "does not change ENV" do
expect { subject }.not_to change { ENV.inspect }
end
end
end
describe "overwrite!" do
let(:env_files) { [fixture_path("plain.env")] }
subject { Dotenv.overwrite!(*env_files) }
it_behaves_like "load"
it_behaves_like "overwrite"
it "initializes the Environment with overwrite: true" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: true)
.and_call_original
subject
end
context "when loading a file containing already set variables" do
let(:env_files) { [fixture_path("plain.env")] }
it "overwrites any existing ENV variables" do
ENV["OPTION_A"] = "predefined"
subject
expect(ENV["OPTION_A"]).to eq("1")
end
end
context "when one file exists and one does not" do
let(:env_files) { [".env", ".env_does_not_exist"] }
it "raises an Errno::ENOENT error" do
expect { subject }.to raise_error(Errno::ENOENT)
end
end
end
describe "with an instrumenter" do
let(:instrumenter) { double("instrumenter", instrument: {}) }
before { Dotenv.instrumenter = instrumenter }
after { Dotenv.instrumenter = nil }
describe "load" do
it "instruments if the file exists" do
expect(instrumenter).to receive(:instrument) do |name, payload|
expect(name).to eq("load.dotenv")
expect(payload[:env]).to be_instance_of(Dotenv::Environment)
{}
end
Dotenv.load
end
it "does not instrument if file does not exist" do
expect(instrumenter).to receive(:instrument).never
Dotenv.load ".doesnotexist"
end
end
end
describe "require keys" do
let(:env_files) { [".env", fixture_path("bom.env")] }
before { Dotenv.load(*env_files) }
it "raises exception with not defined mandatory ENV keys" do
expect { Dotenv.require_keys("BOM", "TEST") }.to raise_error(
Dotenv::MissingKeys,
'Missing required configuration key: ["TEST"]'
)
end
it "raises exception when missing multiple mandator keys" do
expect { Dotenv.require_keys("TEST1", "TEST2") }.to raise_error(
Dotenv::MissingKeys,
'Missing required configuration keys: ["TEST1", "TEST2"]'
)
end
end
describe "parse" do
let(:env_files) { [] }
subject { Dotenv.parse(*env_files) }
context "with no args" do
let(:env_files) { [] }
it "defaults to .env" do
expect(Dotenv::Environment).to receive(:new).with(expand(".env"),
anything)
subject
end
end
context "with a tilde path" do
let(:env_files) { ["~/.env"] }
it "expands the path" do
expected = expand("~/.env")
allow(File).to receive(:exist?) { |arg| arg == expected }
expect(Dotenv::Environment).to receive(:new).with(expected, anything)
subject
end
end
context "with multiple files" do
let(:env_files) { [".env", fixture_path("plain.env")] }
let(:expected) do
{"OPTION_A" => "1",
"OPTION_B" => "2",
"OPTION_C" => "3",
"OPTION_D" => "4",
"OPTION_E" => "5",
"PLAIN" => "true",
"DOTENV" => "true"}
end
it "does not modify ENV" do
subject
expected.each do |key, _value|
expect(ENV[key]).to be_nil
end
end
it "returns hash of parsed key/value pairs" do
expect(subject).to eq(expected)
end
end
it "initializes the Environment with overwrite: false" do
expect(Dotenv::Environment).to receive(:new).with(anything, overwrite: false)
subject
end
context "when the file does not exist" do
let(:env_files) { [".env_does_not_exist"] }
it "fails silently" do
expect { subject }.not_to raise_error
expect(subject).to eq({})
end
end
end
describe "Unicode" do
subject { fixture_path("bom.env") }
it "loads a file with a Unicode BOM" do
expect(Dotenv.load(subject)).to eql("BOM" => "UTF-8")
end
it "fixture file has UTF-8 BOM" do
contents = File.binread(subject).force_encoding("UTF-8")
expect(contents).to start_with("\xEF\xBB\xBF".force_encoding("UTF-8"))
end
end
describe "restore" do
it "restores previously saved snapshot" do
ENV["MODIFIED"] = "true"
Dotenv.restore # save was already called in setup
expect(ENV["MODIFIED"]).to be_nil
end
it "raises an error in threads" do
ENV["MODIFIED"] = "true"
Thread.new do
expect { Dotenv.restore }.to raise_error(ThreadError, /not thread safe/)
end.join
expect(ENV["MODIFIED"]).to eq("true")
end
it "is a noop if nil state provided" do
expect { Dotenv.restore(nil) }.not_to raise_error
end
it "is a noop if no previously saved state" do
# Clear state saved in setup
expect(Dotenv.instance_variable_get(:@diff)).to be_instance_of(Dotenv::Diff)
Dotenv.instance_variable_set(:@diff, nil)
expect { Dotenv.restore }.not_to raise_error
end
it "can save and restore stubbed ENV" do
stub_const("ENV", ENV.to_h.merge("STUBBED" => "1"))
Dotenv.save
ENV["MODIFIED"] = "1"
Dotenv.restore
expect(ENV["STUBBED"]).to eq("1")
expect(ENV["MODIFED"]).to be(nil)
end
end
describe "modify" do
it "sets values for the block" do
ENV["FOO"] = "initial"
Dotenv.modify(FOO: "during", BAR: "baz") do
expect(ENV["FOO"]).to eq("during")
expect(ENV["BAR"]).to eq("baz")
end
expect(ENV["FOO"]).to eq("initial")
expect(ENV).not_to have_key("BAR")
end
end
describe "update" do
it "sets new variables" do
Dotenv.update({"OPTION_A" => "1"})
expect(ENV["OPTION_A"]).to eq("1")
end
it "does not overwrite defined variables" do
ENV["OPTION_A"] = "original"
Dotenv.update({"OPTION_A" => "updated"})
expect(ENV["OPTION_A"]).to eq("original")
end
context "with overwrite: true" do
it "sets new variables" do
Dotenv.update({"OPTION_A" => "1"}, overwrite: true)
expect(ENV["OPTION_A"]).to eq("1")
end
it "overwrites defined variables" do
ENV["OPTION_A"] = "original"
Dotenv.update({"OPTION_A" => "updated"}, overwrite: true)
expect(ENV["OPTION_A"]).to eq("updated")
end
end
end
def expand(path)
File.expand_path path
end
end
================================================
FILE: spec/fixtures/bom.env
================================================
BOM=UTF-8
================================================
FILE: spec/fixtures/exported.env
================================================
export OPTION_A=2
export OPTION_B='\n'
================================================
FILE: spec/fixtures/important.env
================================================
PLAIN=false
OPTION_A=abc
OPTION_B=2
================================================
FILE: spec/fixtures/plain.env
================================================
PLAIN=true
OPTION_A=1
OPTION_B=2
OPTION_C= 3
OPTION_D =4
OPTION_E = 5
================================================
FILE: spec/fixtures/quoted.env
================================================
QUOTED=true
OPTION_A='1'
OPTION_B='2'
OPTION_C=''
OPTION_D='\n'
OPTION_E="1"
OPTION_F="2"
OPTION_G=""
OPTION_H="\n"
================================================
FILE: spec/fixtures/yaml.env
================================================
OPTION_A: 1
OPTION_B: '2'
OPTION_C: ''
OPTION_D: '\n'
================================================
FILE: spec/spec_helper.rb
================================================
require "dotenv"
require "dotenv/autorestore"
require "tmpdir"
def fixture_path(*parts)
Pathname.new(__dir__).join("./fixtures", *parts)
end
# Capture output to $stdout and $stderr
def capture_output(&_block)
original_stderr = $stderr
original_stdout = $stdout
output = $stderr = $stdout = StringIO.new
yield
output.string
ensure
$stderr = original_stderr
$stdout = original_stdout
end
================================================
FILE: test/autorestore_test.rb
================================================
require "active_support" # Rails 6.1 fails if this is not loaded
require "active_support/test_case"
require "minitest/autorun"
require "dotenv"
require "dotenv/autorestore"
class AutorestoreTest < ActiveSupport::TestCase
test "restores ENV between tests, part 1" do
assert_nil ENV["DOTENV"], "ENV was not restored between tests"
ENV["DOTENV"] = "1"
end
test "restores ENV between tests, part 2" do
assert_nil ENV["DOTENV"], "ENV was not restored between tests"
ENV["DOTENV"] = "2"
end
end