Using ENV in MRI-Ruby and JRuby
Image credit: DALL-EENV 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 byENV.
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.