9

If I'm writing a shell script and I want to "source" some external (c-)shell scripts to set up my environment, I can just make calls like this:

source /file/I/want/to/source.csh

I want to replace a shell script that does this with a ruby script. Can I do a similar thing in the ruby script?

Update:

Just tried it with test_script.csh:

#!/bin/csh

setenv HAPPYTIMES True

...and test_script.rb:

#!/usr/bin/env ruby
system "~/test_script.csh"
system "echo $HAPPYTIMES"

Sadly, no HAPPYTIMES as of yet.

3

6 Answers 6

8

Given the following Ruby

# Read in the bash environment, after an optional command.
#   Returns Array of key/value pairs.
def bash_env(cmd=nil)
  env = `#{cmd + ';' if cmd} printenv`
  env.split(/\n/).map {|l| l.split(/=/)}
end

# Source a given file, and compare environment before and after.
#   Returns Hash of any keys that have changed.
def bash_source(file)
  Hash[ bash_env(". #{File.realpath file}") - bash_env() ]
end

# Find variables changed as a result of sourcing the given file, 
#   and update in ENV.
def source_env_from(file)
  bash_source(file).each {|k,v| ENV[k] = v }
end

and the following test.sh:

#!/usr/bin/env bash
export FOO='bar'

you should get:

irb(main):019:0> source_env_from('test.sh')
=> {"FOO"=>"bar"}
irb(main):020:0> ENV['FOO']
=> "bar"

Enjoy!

Sign up to request clarification or add additional context in comments.

Comments

5

The reason this isn't working for you is b/c ruby runs its system commands in separate shells. So when one system command finishes, the shell that had sourced your file closes, and any environment variables set in that shell are forgotten.

If you don't know the name of the sourced file until runtime, then Roboprog's answer is a good approach. However, if you know the name of the sourced file ahead of time, you can do a quick hack with the hashbang line.

% echo sourcer.rb
#!/usr/bin/env ruby
exec "csh -c 'source #{ARGV[0]} && /usr/bin/env ruby #{ARGV[1]}'"
% echo my-script.rb
#!/usr/bin/env ruby sourcer.rb /path/to/file/I/want/to/source.csh
puts "HAPPYTIMES = #{ENV['HAPPYTIMES']}"
% ./my-script.rb
HAPPYTIMES = True

All of these will only help you use the set enviroment variables in your ruby script, not set them in your shell (since they're forgotten as soon as the ruby process completes). For that, you're stuck with the source command.

1 Comment

I'm guessing the only reason this works in, say, csh, is that you're using the same shell instance to interpret the script you're sourcing. So you couldn't source a .csh file from a bash script any more than you could source it from a ruby script...
4

Improving a little on @takeccho's answer... Checks, and a few whistles. First, the sourced environment is cleaned via env -i, which is a safety measure but might be not desired in some cases. Second, via set -a, all variables set in the file are "exported" from the shell and thus imported into ruby. This is useful for simulating/overriding behavior found in environment files used with init scripts and systemd env files.

def ShSource(filename)
  # Inspired by user takeccho at http://stackoverflow.com/a/26381374/3849157
  # Sources sh-script or env file and imports resulting environment
  fail(ArgumentError,"File #{filename} invalid or doesn't exist.") \
     unless File.exist?(filename)

  _newhashstr=`env -i sh -c 'set -a;source #{filename} && ruby -e "p ENV"'`
  fail(ArgumentError,"Failure to parse or process #{filename} environment")\
     unless _newhashstr.match(/^\{("[^"]+"=>".*?",\s*)*("[^"]+"=>".*?")\}$/)

  _newhash=eval(_newhashstr)
   %w[ SHLVL PWD _ ].each{|k|_newhash.delete(k) }
  _newhash.each{|k,v| ENV[k]=v } # ENV does not have #merge!
end

Theory of operation: When ruby outputs the ENV object using p, it does so in a way that ruby can read it back in as an object. So we use the shell to source the target file, and ruby (in a sub-shell) to output the environment in that serializable form. We then capture ruby's output and eval it within our ruby-process. Clearly this is not without some risk, so to mitigate the risk, we (1) validate the filename that is passed in, and (2) validate using a regexp that the thing we get back from the ruby-subshell is, in fact, a serializable hash-string. Once we're sure of that, we do the eval which creates a new hash. We then "manually" merge the hash with ENV, which is an Object and not a regular Hash. If it were a Hash, we could have used the #merge! method.

EDIT: sh -a exported things like PATH. We must also remove SHLVL and PWD before the hash-merge.

Comments

3

I had have same probrem. and I resolve like below.

#!/usr/local/bin/ruby

def source(filename)
  ENV.replace(eval(`tcsh -c 'source #{filename} && ruby -e "p ENV"'`))
end

p "***old env*****************************"
p ENV
source "/file/I/want/to/source.csh"
p "+++new env+++++++++++++++++++++++++++++"
p ENV

'eval' is a very powerful method. It jump over the process easily.

1 Comment

You need to be very careful if filename actually is what you expect it to be; if this is user input, then it's a huge security leak ... You usually don't want to source arbitrary files, so a more secure way to do this might be to assume filename is a symbol, and look that up in a Hash (and if it doesn't exist, throw an error).
1

You are going to have to write a function to run something like the following, and capture the output ("backtick" operation):

/bin/csh -e '. my_script ; env'

Loop on each line, match against something like

/^(\w+)=(.*)$/

Then use the first match capture as the var name, and the second capture as the var value.

(yes, I'm hedging on the fact that I know Perl way better than Ruby, but the approach would be the same)

1 Comment

Actually, what I did capture ALL environment variables after running the script. IFF the assignments in the "script" have no interpolated values, only string literals, you could get what you want be merely reading the file, rather than having to run it on a pipe (backtick).
-5
system 'source /file/I/want/to/source.sh'

Not sure that this will do what you want though. It will execute the source command in a subshell. Try it and see it it does what you're after.

3 Comments

I tried this and it does not appear to do what I'm after. I'm trying to get the environment variables set by the shell script into the environment the ruby script is running in.
environment changes in a child process do not affect the parent
I thought that might be the case. Sorry for wasting your time.

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.