Advertisement

ENV makes the environment variables of the running process available within a Ruby-script. But there is a subtle difference in implementation between MRI-Ruby and JRuby. Unfortunately that difference broke some aruba-builds on Travis.

But before we get into the details, let’s start with an introduction. To read the value of an environment variable in Ruby, use ENV['VARIABLE'] in your application.

puts ENV['HOME']
# => /home/user

To change the value of an environment variable, use ENV['VARIABLE'] = 'value'.

ENV['MY_VARIABLE'] = 'value'
puts ENV['MY_VARIABLE']
# => value

It’s important that the value is a String, otherwise Ruby will raise a TypeError.

ENV['MY_VARIABLE'] = 1
# => TypeError: no implicit conversion of Fixnum into String

Getting started

Let’s start an irb-session first and print the value of the HOME-variable, which contains the path to your HOME-directory – at least on Unix-/Linux-operating systems.

$ irb
irb(main):001:0>

On a UNIX-/Linux-/Mac-operating system you should see something like this.

puts ENV['HOME']
# => /home/user

Now, create a new environment variable by using the code found below:

    • Pro-Tip
  • You need to use Strings as variable values. Everything else is not accepted by ENV.
ENV['MY_VARIABLE'] = '1'
puts ENV['MY_VARIABLE']
# => 1

The Setup

On my local machine I have/had the following rubies installed. You may get different results if you use a different version.

MRI

$ ruby --version
# => ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]

JRuby

# at first
$ jruby --version
# => JRuby 1.7.20.1 (1.9.3p551) 2015-06-10 d7c8c27 on OpenJDK 64-Bit Server VM 1.7.0_79-b14 +jit [linux-amd64]

# after an upgrade
$ jruby --version
# => JRuby 9.0.0.0 (2.2.2) 2015-07-21 e10ec96 OpenJDK 64-Bit Server VM 24.85-b03 on 1.7.0_85-b01 +jit [linux-amd64]

The differences

Ok. That was easy. Now let’s switch to the fun part and check what ENV really is. Please start a JRuby irb-session for this as well. On my system I need to run jirb to start that session. That might be different on your machine.

$ jirb
jirb(main):001:0>

Class of “ENV”

Let’s check the Class of ENV first.

MRI

ENV.class
# => Object

JRuby

ENV.class
# => Hash

Oh. That’s the first difference, but not a problem at all.

Converting “ENV” to “Hash”

There are quite a few use cases where you need to convert ENV to an Hash. We – at aruba – use this to capture the “old” environment, run some ruby code and clean up the environment after that:

def local_environment(&block)
  # Capture old environment
  old_env = ENV.to_h
  # Run code
  block.call if block_given?
ensure
  # Remove all existing variables including new ones created by code block
  ENV.clear
  # Update environment with old environment
  ENV.update old_env
end

Besides #to_h, you can also use #to_hash for this. Reading the latest documentation as of this writing, both should create “a hash with a copy of the environment variables”. But unfortunately this is not true for JRuby. Let’s check this by invoking #object_id on the results.

MRI

First let’s check this for MRI-ruby. The object ID is different for ENV and both Hashes. Perfect!

ENV.object_id
# => 17981260
ENV.to_h.object_id
# => 22183040
ENV.to_hash.object_id
# => 22148380

JRuby

And now let’s check this for JRuby. The object ID is different for ENV and for the Hash created by #to_hash. The Hash created by #to_h has the same object ID like the one of ENV. Uh… That might become a problem.

ENV.object_id
# => 2042
ENV.to_h.object_id
# => 2042
ENV.to_hash.object_id
# => 2044

The Problem

In aruba we need to deal with ENV a lot and we also need to be compatible with MRI- and JRuby. We use code similar to the one given above, to make sure ENV is not “polluted” by user code. We decided to use #to_h to capture the old environment – at least until we found out, that we need to use #to_hash to be compatible with MRI-Ruby 1.8.7.

    • Pro-Tip
  • You can paste the code found below in your (j)irb-sessions to try it yourself.

MRI

With MRI-ruby everything is fine. The ENV is cleaned up after the code block has run.

class MyClass
  def with_env(&block)
    # Capture old environment
    old_env = ENV.to_h
    # Run code
    block.call if block_given?
  ensure
    # Remove all existing variables including new ones created by code block
    ENV.clear
    # Update environment with old environment
    ENV.update old_env
  end
end

ENV['VARIABLE1'] = '0'

MyClass.new.with_env do
  ENV['VARIABLE1'] = '2'

  puts ENV['VARIABLE1']
  # => 2
end

puts ENV['VARIABLE1']
# => 0

JRuby

This is not true for JRuby. old_env contains the same object like ENV. The problem is the line where #clear is called on ENV. We use this method to get rid of new environment variables which were created by the code block. But if ENV contains the same object like old_env, you will clear both if you call #clear on ENV. That’s the reason why ENV['VARIABLE1'] returns nil at the end of the example.

class MyClass
  def with_env(&block)
    old_env = ENV.to_h
    block.call if block_given?
  ensure
    ENV.clear
    ENV.update old_env
  end
end

ENV['VARIABLE1'] = '0'

MyClass.new.with_env do
  ENV['VARIABLE1'] = '2'

  puts ENV['VARIABLE1']
  # => 2
end

puts ENV['VARIABLE1']
# => nil

ENV
# => {}

The solution

To solve the problem, either use #to_hash or #dup to get a “real” copy of ENV which is not cleared, if you call #clear on ENV.

Use “#to_hash”

class MyClass
  def with_env(&block)
    old_env = ENV.to_hash
    block.call if block_given?
  ensure
    ENV.clear
    ENV.update old_env
  end
end

Use “#dup”

class MyClass
  def with_env(&block)
    old_env = ENV.to_h.dup
    block.call if block_given?
  ensure
    ENV.clear
    ENV.update old_env
  end
end

Conclusion

If you need to use something similar to the code given above in your application, make sure you either use #to_hash or #dup in JRuby. There is also an issue at JRuby’s bugtracker on Github.

Discussion

If you found a mistake in this article or would like to contribute some content to it, please file an issue in this Git Repository

Disclaimer

The contents of this article are put together to the best of the authors' knowledge, but it cannot be guaranteed that it's always accurate in any environment. It is up to the reader to make sure that all information found in this article, does not do any damage to the reader's working environment or wherever this information is applied to. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, arising from, out of or in connection with this article. Please also note the information given on the Licences' page.