TL;DR
This issue is largely shell-specific, and is casued by the fact that most shells require environment variables to be strings rather than arrays. With many shells, that makes shell arrays inaccessible via Ruby's ENV module.
Exported arrays from Fish can be accessed through ENV as indexed arrays, but Fish currently has no explicit support for associative arrays. On the other hand, Bash supports both types of arrays but doesn't appear to export arrays to the environment at all even though the syntax allows for it. To make matters worse, export FOO=(foo bar baz)
won't return a non-zero exit status. As a result, you can't directly access Bash arrays through Ruby's ENV module although workarounds exist.
Skip to the end if you just want to see my preferred solution using ARGV, but I think the other approaches I discuss first have their own merits and potential utility value. In other words, if you really need access through Ruby's ENV rather than ARGV, you may need to pass the definition of the array into your environment rather than accessing the shell array directly. That's why I talk through those approaches first.
Indexed Arrays in Fish
In the Fish shell, all variables are intrinsically arrays anyway, so you can do something like:
set -gx FOO foo bar baz
ruby -e 'p ENV["FOO"]; p ENV["FOO"].split.last'
and get sensible results, but it's essentially up to you to split the strings or understand how the values are encoded by such shells to find your preferred index or treat multi-word shellwords as separate array elements. Your question wasn't about the Fish shell, but I thought it provided a useful illustration of the fact that shell arrays are (by definition) handled in shells-specific ways, so you might need to take that into account if you must do this through environment variables.
For Bash and Zsh, there are definitely better approaches than relying on the direct export of a shell array to the environment, but in most cases you simply shouldn't rely on Ruby's ENV to access shell arrays from the environment since such variables often can't be directly exported in many Bourne-based shells.
Arrays from Ruby in Bash-Like Shells
Bash and Zsh provide declare -a
, declare -A
, and declare -p
which can help up to a point, but still require you to work around the fact that shell arrays aren't usually exposed as normal strings in the shell's environment even when exported, and will therefore be inaccessible to Ruby's ENV.
For example, you could do something like the following in Bash/Zsh to essentially call declare
to re-assign the definition of the array stored in FOO to another environment variable such as RUBY_FOO. This definition, unlike the array itself, can be exported to sub-shells and Ruby's ENV as an indirect way to leverage shell builtins and expansions to access your array elements from inside Ruby.
FOO=(foo bar baz)
export RUBY_FOO=$(declare -p FOO)
ruby -e 'puts %x(#{ENV["RUBY_FOO"]}; printf "%s\n" ${FOO[@]})'
This will correctly return "foo\nbar\nbaz\n"
. For more complex word-splitting, you can look into the Shellwords module or do some of your own parsing. As an example, consider this array that contains two words in every element, potentially subjecting the words to unexpected word splitting or expansions unless properly quoted.
FOO=("foo bar" "baz quux")
export RUBY_FOO=$(declare -p FOO)
ruby -e 'p %x(#{ENV["RUBY_FOO"]}; printf "%s\n" "${FOO[@]}").split("\n")'
This time, you'll correct get back ["foo bar", "baz quux"]
, which is pretty good considering the limitations of the approach. You could also manually parse the output of declare -p
to convert things into a Ruby collection, but that seems like more trouble than it's worth when there's a much easier way!
Use Ruby's ARGV to Pass Array Elements as Arguments
Indexed Arrays
A much easier approach, regardless of your choice of shell, is to simply pass your indexed array as shell-quoted arguments to your script. For example:
FOO=("foo bar" "baz quux")
ruby -e 'ARGV.map { p _1 }' "${FOO[@]}"
This will print each of your array elements with minimal fuss, and no need for indirection, parsing, or anything other than the quoting needed to properly form the shell's array values in the first place.
Associative Arrays
Note that a very similar approach also works for shells that support associative arrays. For example, let's unset FOO and explicitly recast it as an associative array. Then we'll pass the keys and values into ARGV using shell expansions.
unset FOO
declare -A FOO=("foo bar" 200 "baz quux" 404)
ruby -e 'size = ARGV.count / 2
p ARGV[...size].zip(ARGV[-(size)..]).to_h' \
"${!FOO[@]}" "${FOO[@]}"
This returns a pretty Hash like {"foo bar"=>"200", "baz quux"=>"404"}
, but that's really just an implementation detail chosen for this example. Passing the keys and values of the associative array is easy; it's the effort of splitting, joining, and formatting the associative array's keys and values of the shell's associative array in Ruby that makes it look harder than it is. Again, that's just an implementation detail.
All we've really had to do here is pass the associative keys from the shell's "${!FOO[@]}"
expansion plus the stored values from the expansion of "${FOO[@]}"
. The rest is just zipping up the matching key/value pairs into sub-arrays, and then converting the result to a Hash!