42

How do I export an environment variable from within a Ruby script to the parent shell? For example, implementing a naïve implementation of the read Bash builtin:

#!/usr/bin/ruby

varname = ARGV[0]
ENV[varname] = STDIN.gets  # but have varname exported to the parent process
1
  • 2
    I've seen a similar question but I'm not satisfied with the platform-specific answer. Commented Apr 18, 2010 at 0:09

5 Answers 5

41

You can't export environment variables to the shell the ruby script runs in, but you could write a ruby script that creates a source-able bash file.

For example

% cat set_var.rb
#!/usr/bin/env ruby
varname = ARGV[0]
puts "#{varname}=#{STDIN.gets.chomp}"
% set_var.rb FOO
1
FOO=1
% set_var.rb BAR > temp.sh ; . temp.sh
2
% echo $BAR
2
%

Another alternative is that using ENV[]= does set environment variables for subshells opened from within the ruby process. For example:

outer-bash% cat pass_var.rb
#!/usr/bin/env ruby
varname = ARGV[0]
ENV[varname] = STDIN.gets.chomp
exec '/usr/bin/env bash'
outer-bash% pass_var.rb BAZ
quux
inner-bash% echo $BAZ
quux 

This can be quite potent if you combine it with the shell's exec command, which will replace the outer-shell with the ruby process (so that when you exit the inner shell, the outer shell auto-exits as well, preventing any "I thought I set that variable in this shell" confusion).

# open terminal
% exec pass_var.rb BAZ
3
% echo $BAZ
3
% exit
# terminal closes
Sign up to request clarification or add additional context in comments.

Comments

38

Simple answer: You can't.

Longer answer: You can't, unless the operating environment provides hooks to do so. Most do not. The best you can usually do is print out the assignments you want done and have the parent execute them.

4 Comments

look at peterk's answer it works on windows too. I tried it with rake's sh executing dos command that requires env set within rake.
@kite: "to the parent shell"
as an alternative, consider setting a credentials dotfile in an appropriate directory
see @rampion's answer for possible workaround. It could be as simple as putting exec '/usr/bin/env bash' at the end of your script/code.
10

I just tried this and it looks good.

cmd = "echo \"FOO is \\\"$FOO\\\"\"";                                
system(cmd);

# Run some Ruby code (same program) in the child process
fork do
    puts "In child process. parent pid is #$$"
    ENV['FOO']='foo in sub process';
    system(cmd);
    exit 99
end
child_pid = Process.wait
puts "Child (pid #{child_pid}) terminated with status #{$?.exitstatus}"

system(cmd);

This seems to work well - at least on MacOSX

I get

FOO is ""
In child process. parent pid is 1388
FOO is "foo in sub process"
Child (pid 1388) terminated with status 99
FOO is ""

Seems nice in it restores prior state automatically

Ok - now tried a different one as this doesn't spawn 2 subprocesses

Use Process.spawn(env,command)

pid = Process.spawn({ 'FOO'=>'foo in spawned process'}, cmd );
pid = Process.wait();  

This acts like the C system call and allows you to specify pipes and all that other stuff too.

2 Comments

To save me the time, does anyone know if this works on Linux? Also, I don’t understand how this works, given how child processes inherit their parents’ env but generally cannot modify it. IIRC.
interesting, great for spawning processes, but doesn't set it globally in the parent shell.
0

Yes Virginia, there is a Santa Clause

I know this is rather late to the game, but maybe things have changed since the original answer that was accepted. The reality is that now you can source from a non-bash script while running bash. That script simply needs to print the lines you want sourced, and doesn't need to print them into a temporary file for you to source.

TLDR:

My pathcleaner.rb script (used to remove duplicate entries from my $PATH):

#!/usr/bin/env -S ruby
path = ENV['PATH'].strip.split(/:/).uniq.join(':')
puts "PATH=\"#{path}\""

The command that I run to execute that script, while actually affecting my current environment (instead of just the environment existing within the context of the ruby script):

$ source <(pathcleaner.rb)

The Longer Explanation

So, while it is true that the typical source mechanism of . scriptfile.sh does not work unless the scriptfile is a bash (or other compatible shell) script, at some point bash added the concept of process substitution (see postscript far below for an examination of when):

Process Substitution source

Process substitution allows a process's input or output to be referred to using a filename. It takes the form of <(list) or >(list). The process list is run asynchronously, and its input or output appears as a filename. This filename is passed as an argument to the current command as the result of the expansion. If the >(list) form is used, writing to the file will provide input for list. If the <(list) form is used, the file passed as an argument should be read to obtain the output of list. Note that no space may appear between the < or > and the left parenthesis, otherwise the construct would be interpreted as a redirection. Process substitution is supported on systems that support named pipes (FIFOs) or the /dev/fd method of naming open files.

What this all means is that the process you create by running a non-bash script can be used as the source for the "contents" of a fictitious or "virtual" file that bash pretends is the source of input to the source command.

Let's dig into what this is doing behind the scenes. If we use the following command, we can see what bash is actually doing with my pathcleaner.rb input script discussed above:

$ echo <(pathcleaner.rb)
/dev/fd/63

OK, so the <(pathcleaner.rb) argument is replaced with the file-descriptor path /dev/fd/63. What happens when we cat the <(pathcleaner.rb) argument instead?

$ cat <(pathcleaner.rb)
PATH="~/bin:/usr/lib/postgresql/11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:~/.rvm/bin"

It shows us that the virtual file tied to the file descriptor contains the output of the process from my pathcleaner.rb script. The really neat thing about this is that despite its resemblance to simple input redirection, this is a completely different animal and can be used with any command, and in place of a filename, anywhere in your command line. Let me illustrate.

Let's say I've created three ruby scripts: one.rb, two.rb, and three.rb. Now if I wanted to try regular input redirection on all three at once, what happens?

$ cat <./one.rb <./two.rb <./three.rb
#!/usr/bin/env -S ruby
puts 'THREE="Third Script Output"'

Wait a minute! Only the last input redirection is processed. It seems that each input redirection takes over as the input for the command, and that the others are simply ignored. What happens if we actually use the process substitution to get what we really wanted with this command?

$ cat <(cat ./one.rb) <(cat ./two.rb) <(cat ./three.rb)
#!/usr/bin/env -S ruby
puts 'ONE="First Script Output"'
#!/usr/bin/env -S ruby
puts 'TWO="Second Script Output"'
#!/usr/bin/env -S ruby
puts 'THREE="Third Script Output"'

Note: Yes, I know that we could simply accomplish this specific task by simply passing the filenames to cat without redirection. This is just an example demonstrating the functionality.

So what is clear from this is that you can put any command inside the parentheses in the process substitution, and have as many process substitutions as you like (within the OS limit of maximum available file descriptors, which is 9,223,372,036,854,775,807 on my Linux system), and your command line will treat it as though each instance was a file containing the output of the encapsulated command instead.

So, to make a long story long, you can:

  • Write a non-bash script to programmatically generate the commands you want bash to execute. For example, my pathcleaner.rb script.
  • Pass that script inside a process substitution argument. For example: <(pathcleaner.rb), or <(/path/to/pathcleaner.rb) if it is not on the path.
  • And have any command treat that argument as a file for it to process. For example telling source to execute the output of my ruby script with source <(pathcleaner.rb)

PS

After doing a bit of research, it looks like this feature has been in bash since 1992 and was in the korn shell as early as 1988. I'm guessing that it's just not a feature that many people know about, hence the prevalence of suggestions to create an intermediate file with your script and then source that file in the other answers here.

That said, to his credit, rampion, who has the highest rated answer so far (from 2010) does mention this feature briefly in a 2022 comment on the answer provided by seeder, but he refers to it as "shell redirection", and shell redirection is a much more limited functionality than what we actually have here with process substitution as demonstrated with my input redirection example above.

2 Comments

neat, generate dynamic script and execute. i think syntactic sugar of @rampion
Yep, and we Ruby fans love the syntactic sugar. =)
-1

What about in ruby printing out standard export code :

puts "export MYVAR=value"

and then using shell backtick to get it executed at shell comands:

$ `./myscript.rb` 

this will take the output of the script and execute it, works in modern shells like bash and zsh

3 Comments

Unfortunately, that doesn't work either, because it is still not acting directly in the parent shell.
why don't you try out what you suggest before suggesting it
the way to make this work would be to use shell redirection: source <(./myscript.rb)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.