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
String
s 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 Hash
es. 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.