diff --git a/.travis.yml b/.travis.yml index aa0836e9..827df5f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: ruby rvm: + - 2.1.0 - 2.0.0 - 1.9.3 - 1.8.7 - ree + - jruby-19mode + - rbx +bundler_args: --without=guard \ No newline at end of file diff --git a/Changelog.md b/Changelog.md index b2ff05e9..25865519 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,17 @@ # Changelog +## 0.10.0 - Feb 22, 2014 + +* Add support for executing interpolated commands. (Ruby >= 1.9 only) + + HEAD_SHA=$(git rev-parse HEAD) + +* Add `dotenv_role` option in Capistrano. + + set :dotenv_role, [:app, web] + +* Add `Dotenv.overload` to overwrite existing environment values. + ## 0.9.0 - Aug 29, 2013 * Add support for variable expansion. diff --git a/Gemfile b/Gemfile index 79de2356..8a52d582 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,12 @@ source 'https://rubygems.org' gemspec :name => 'dotenv' -gem 'guard-rspec' -gem 'guard-bundler' -gem 'rb-fsevent' +group :guard do + gem 'guard-rspec' + gem 'guard-bundler' + gem 'rb-fsevent' +end + +platforms :rbx do + gem 'rubysl', '~> 2.0' # if using anything in the ruby standard library +end \ No newline at end of file diff --git a/README.md b/README.md index 6fe36c34..faa859eb 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# dotenv [![Build Status](https://secure.travis-ci.org/bkeepers/dotenv.png)](https://travis-ci.org/bkeepers/dotenv) +# dotenv [![Build Status](https://secure.travis-ci.org/bkeepers/dotenv.png?branch=master)](https://travis-ci.org/bkeepers/dotenv) Dotenv loads environment variables from `.env` into `ENV`. Storing [configuration in the environment](http://www.12factor.net/config) is one of the tenets of a [twelve-factor app](http://www.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 load variables from a `.env` file into ENV when the environment is bootstrapped. +But it is not always practical to set environment variables on development machines or continuous integration servers where multiple projects are run. Dotenv load variables from a `.env` file into `ENV` when the environment is bootstrapped. ## Installation ### Rails -Add this line to your application's Gemfile: +Add this line to the top of your application's Gemfile: ```ruby gem 'dotenv-rails', :groups => [:development, :test] @@ -18,13 +18,19 @@ gem 'dotenv-rails', :groups => [:development, :test] And then execute: - $ bundle +```shell +$ bundle +``` + +It should be listed in the Gemfile before any other gems that use environment variables, otherwise those gems will get initialized with the wrong values. ### Sinatra or Plain ol' Ruby Install the gem: - $ gem install dotenv +```shell +$ gem install dotenv +``` As early as possible in your application bootstrap process, load `.env`: @@ -33,6 +39,12 @@ require 'dotenv' Dotenv.load ``` +Alternatively, you can use the `dotenv` executable to launch your application: + +```shell +$ dotenv ./script.py +``` + To ensure `.env` is loaded in rake, load the tasks: ```ruby @@ -74,13 +86,26 @@ config.fog_directory = ENV['S3_BUCKET'] ## Capistrano integration -In your `config/deploy.rb` file: +If you want to use Dotenv with Capistrano in your production environment, make sure the dotenv gem is included in your Gemfile `:production` group. + +### Capistrano version 2.x.x + +Add the gem to your `config/deploy.rb` file: ```ruby require "dotenv/capistrano" ``` -It will symlink the `.env` located in `/path/to/shared` in the new release. +It will symlink the `.env` located in `/path/to/shared` in the new release. + + +### Capistrano version 3.x.x + +Just add `.env` to the list of linked files, for example: + +```ruby +set :linked_files, %w{config/database.yml .env} +``` ## Should I commit my .env file? diff --git a/bin/dotenv b/bin/dotenv index 5c37b26a..ec295cbf 100755 --- a/bin/dotenv +++ b/bin/dotenv @@ -1,12 +1,11 @@ #!/usr/bin/env ruby -require "dotenv" +require 'dotenv' begin Dotenv.load! rescue Errno::ENOENT => e - warn e.message - exit 1 + abort e.message else - exec *ARGV + exec *ARGV unless ARGV.empty? end diff --git a/dotenv-rails.gemspec b/dotenv-rails.gemspec index 463d19c1..6ef242ea 100644 --- a/dotenv-rails.gemspec +++ b/dotenv-rails.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |gem| gem.description = %q{Autoload dotenv in Rails.} gem.summary = %q{Autoload dotenv in Rails.} gem.homepage = "https://github.com/bkeepers/dotenv" + gem.license = 'MIT' gem.files = ["lib/dotenv-rails.rb"] gem.name = "dotenv-rails" diff --git a/dotenv.gemspec b/dotenv.gemspec index 4c966c16..d78dffd0 100644 --- a/dotenv.gemspec +++ b/dotenv.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |gem| gem.description = %q{Loads environment variables from `.env`.} gem.summary = %q{Loads environment variables from `.env`.} gem.homepage = "https://github.com/bkeepers/dotenv" + gem.license = 'MIT' gem.files = `git ls-files`.split($\) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } diff --git a/lib/dotenv.rb b/lib/dotenv.rb index 6f8c4968..2ffea317 100644 --- a/lib/dotenv.rb +++ b/lib/dotenv.rb @@ -8,6 +8,14 @@ def self.load(*filenames) end end + # same as `load`, but will override existing values in `ENV` + def self.overload(*filenames) + default_if_empty(filenames).inject({}) do |hash, filename| + filename = File.expand_path filename + hash.merge(File.exists?(filename) ? Environment.new(filename).apply! : {}) + end + end + # same as `load`, but raises Errno::ENOENT if any files don't exist def self.load!(*filenames) load( diff --git a/lib/dotenv/capistrano.rb b/lib/dotenv/capistrano.rb index dc6dd02d..0f49b7e8 100644 --- a/lib/dotenv/capistrano.rb +++ b/lib/dotenv/capistrano.rb @@ -1,5 +1,11 @@ -require 'dotenv/capistrano/recipes' +require 'capistrano/version' -Capistrano::Configuration.instance(:must_exist).load do - before "deploy:finalize_update", "dotenv:symlink" +if defined?(Capistrano::VERSION) && Capistrano::VERSION >= '3.0' + raise 'Please read https://github.com/bkeepers/dotenv#capistrano-integration to update your dotenv configuration for new Capistrano version' +else + require 'dotenv/capistrano/recipes' + + Capistrano::Configuration.instance(:must_exist).load do + before "deploy:finalize_update", "dotenv:symlink" + end end diff --git a/lib/dotenv/capistrano/recipes.rb b/lib/dotenv/capistrano/recipes.rb index 32293048..1e1b226d 100644 --- a/lib/dotenv/capistrano/recipes.rb +++ b/lib/dotenv/capistrano/recipes.rb @@ -1,9 +1,11 @@ Capistrano::Configuration.instance(:must_exist).load do _cset(:dotenv_path){ "#{shared_path}/.env" } + symlink_args = (role = fetch(:dotenv_role, nil) ? {:roles => role} : {}) + namespace :dotenv do desc "Symlink shared .env to current release" - task :symlink, roles: :app do + task :symlink, symlink_args do run "ln -nfs #{dotenv_path} #{release_path}/.env" end end diff --git a/lib/dotenv/environment.rb b/lib/dotenv/environment.rb index 4a1d146b..d4032e56 100644 --- a/lib/dotenv/environment.rb +++ b/lib/dotenv/environment.rb @@ -1,7 +1,13 @@ require 'dotenv/format_error' +require 'dotenv/substitutions/variable' +if RUBY_VERSION > '1.8.7' + require 'dotenv/substitutions/command' +end module Dotenv class Environment < Hash + @@substitutions = Substitutions.constants.map { |const| Substitutions.const_get(const) } + LINE = / \A (?:export\s+)? # optional export @@ -17,15 +23,6 @@ class Environment < Hash (?:\s*\#.*)? # optional comment \z /x - VARIABLE = / - (\\)? - (\$) - ( # collect braces with var for sub - \{? # allow brace wrapping - ([A-Z0-9_]+) # match the variable - \}? # closing brace - ) - /xi def initialize(filename) @filename = filename @@ -47,15 +44,8 @@ def load value = value.gsub(/\\([^$])/, '\1') end - # Process embedded variables - value.scan(VARIABLE).each do |parts| - if parts.first == '\\' - replace = parts[1...-1].join('') - else - replace = self.fetch(parts.last) { ENV[parts.last] } - end - - value = value.sub(parts[0...-1].join(''), replace || '') + @@substitutions.each do |proc| + value = proc.call(value, self) end self[key] = value @@ -72,5 +62,9 @@ def read def apply each { |k,v| ENV[k] ||= v } end + + def apply! + each { |k,v| ENV[k] = v } + end end end diff --git a/lib/dotenv/substitutions/command.rb b/lib/dotenv/substitutions/command.rb new file mode 100644 index 00000000..75f6fcb1 --- /dev/null +++ b/lib/dotenv/substitutions/command.rb @@ -0,0 +1,32 @@ +module Dotenv + module Substitutions + module Command + class << self + + INTERPOLATED_SHELL_COMMAND = / + (?\\)? + \$ + (? # 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 |*| + command = $~[:cmd][1..-2] # Eliminate opening and closing parentheses + + if $~[:backslash] + $~[0][1..-1] + else + `#{command}`.chomp + end + end + end + end + + end + end +end diff --git a/lib/dotenv/substitutions/variable.rb b/lib/dotenv/substitutions/variable.rb new file mode 100644 index 00000000..f73aced5 --- /dev/null +++ b/lib/dotenv/substitutions/variable.rb @@ -0,0 +1,35 @@ +module Dotenv + module Substitutions + module Variable + class << self + + VARIABLE = / + (\\)? + (\$) + ( # collect braces with var for sub + \{? # allow brace wrapping + ([A-Z0-9_]+) # match the variable + \}? # closing brace + ) + /xi + + def call(value, env) + # Process embedded variables + value.scan(VARIABLE).each do |parts| + if parts.first == '\\' + replace = parts[1...-1].join('') + else + replace = env.fetch(parts.last) { ENV[parts.last] } + end + + value = value.sub(parts[0...-1].join(''), replace || '') + end + + value + end + end + + end + + end +end diff --git a/lib/dotenv/version.rb b/lib/dotenv/version.rb index b19ed3dc..3a69ad7a 100644 --- a/lib/dotenv/version.rb +++ b/lib/dotenv/version.rb @@ -1,3 +1,3 @@ module Dotenv - VERSION = '0.9.0' + VERSION = '0.10.0' end diff --git a/spec/dotenv/environment_spec.rb b/spec/dotenv/environment_spec.rb index 7567ce72..e74996d7 100644 --- a/spec/dotenv/environment_spec.rb +++ b/spec/dotenv/environment_spec.rb @@ -29,6 +29,19 @@ end end + describe 'apply!' do + it 'sets variables in the ENV' do + subject.apply + expect(ENV['OPTION_A']).to eq('1') + end + + it 'overrides defined variables' do + ENV['OPTION_A'] = 'predefined' + subject.apply! + expect(ENV['OPTION_A']).to eq('1') + end + end + it 'parses unquoted values' do expect(env('FOO=bar')).to eql('FOO' => 'bar') end @@ -92,7 +105,7 @@ expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz") end - it 'parses varibales with "." in the name' do + it 'parses variables with "." in the name' do expect(env('FOO.BAR=foobar')).to eql('FOO.BAR' => 'foobar') end @@ -125,6 +138,31 @@ expect(env("foo='ba#r'")).to eql('foo' => 'ba#r') end + if RUBY_VERSION > '1.8.7' + it 'parses shell commands interpolated in $()' do + expect(env('ruby_v=$(ruby -v)')).to eql('ruby_v' => RUBY_DESCRIPTION) + end + + it 'allows balanced parentheses within interpolated shell commands' do + expect(env('ruby_v=$(echo "$(echo "$(echo "$(ruby -v)")")")')).to eql('ruby_v' => RUBY_DESCRIPTION) + end + + it "doesn't interpolate shell commands when escape says not to" do + expect(env('ruby_v=escaped-\$(ruby -v)')).to eql('ruby_v' => 'escaped-$(ruby -v)') + 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 + + # 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 + require 'tempfile' def env(text) file = Tempfile.new('dotenv') diff --git a/spec/dotenv_spec.rb b/spec/dotenv_spec.rb index a5cd3684..0c4eceec 100644 --- a/spec/dotenv_spec.rb +++ b/spec/dotenv_spec.rb @@ -6,7 +6,7 @@ let(:env_files) { [] } it 'defaults to .env' do - Dotenv::Environment.should_receive(:new).with(expand('.env')). + expect(Dotenv::Environment).to receive(:new).with(expand('.env')). and_return(double(:apply => {})) subject end @@ -17,8 +17,8 @@ it 'expands the path' do expected = expand("~/.env") - File.stub(:exists?){ |arg| arg == expected } - Dotenv::Environment.should_receive(:new).with(expected). + allow(File).to receive(:exists?){ |arg| arg == expected } + expect(Dotenv::Environment).to receive(:new).with(expected). and_return(double(:apply => {})) subject end @@ -82,6 +82,17 @@ end end + describe 'overload' do + it 'overrides any existing ENV variables' do + ENV['OPTION_A'] = 'predefined' + path = fixture_path 'plain.env' + + Dotenv.overload(path) + + expect(ENV['OPTION_A']).to eq('1') + end + end + def fixture_path(name) File.join(File.expand_path('../fixtures', __FILE__), name) end