Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Jeff-Russ/79303bf02049086afb15fc0903195548 to your computer and use it in GitHub Desktop.
Save Jeff-Russ/79303bf02049086afb15fc0903195548 to your computer and use it in GitHub Desktop.

Run (almost) Any Language From a Bash Script

You probably know that when you make a bash script you give it a shebang like one of these:

#!/bin/sh
#!/bin/bash
#!/usr/local/bin/bash 
#!/usr/bin/env bash

and then it's run-able even if you default shell is not one of these interpretors. You can also choose and different language and it will run from you terminal prompt or another script just as if it's a shell script in your default environment:

#!/usr/bin/ruby
#!/usr/local/bin/node
#!/usr/local/bin/coffee 
#!/usr/bin/php
#!/usr/bin/python
#!/usr/bin/env ruby
#!/usr/bin/env node
#!/usr/bin/env coffee
#!/usr/bin/env php
#!/usr/bin/env python

after one of these is placed atop your script you can start using the other language.

Pipin' Hot Code!

What you might not know is that even after you define your interpretor for a file you can redirect some foreign code to another interpretor:

#!/bin/bash

echo "puts 'Hello from Ruby!'" | /usr/bin/env ruby

Above, we are using echo simply to deliver some code to another interpretor. Instead of echoing to you at that terminal the | is redirecting the code via standard stream, which is basically just syntax-less text, to /usr/bin/env ruby. We could have used printf instead but it would look at any escaped characters like \n and replace them with what the mean before the code is sent. That means if you want to actually use the \n in the Ruby code you won't have it.

A Multi-language Code Redirecting Bash Function

Armed with this, we can create a bash function that does this for us and works in any number of languages:

run () {
  local sh="/usr/bin/env sh"
  local bash="/usr/bin/env bash"
  local rb="/usr/bin/env ruby"
  local js="/usr/bin/env node"
  local cs="/usr/bin/env coffee"
  local php="/usr/bin/env php"
  local py="/usr/bin/env python"

  local lang="${!1}"
  shift
  echo "$@" | $lang # DON'T wrap $lang in quotes!
}

The last line just did what we were doing before but now it uses a variable for the destination interpretor. The first arg will take any of those local variable names, which are mostly the languages file extensions, and replace it with the value. This replacing is what that ! is doing in "${!1}". Here it is in use:

run rb "puts 'Hello from Ruby!'"
run js "console.log('Hello from JavaScript!')"
run cs "console.log 'Hello from CoffeeScript!'"
run php '<?php print "Hello from php!\n"; ?>'
run py "print 'Hello from Python!'"

This is great! We can even capture the response instead of just sending it straight out to the user at the terminal:

rb_response=$(run rb "puts 'Hello from Ruby!'")
js_response=$(run js "console.log('Hello from JavaScript!')")
cs_response=$(run cs "console.log 'Hello from CoffeeScript!'")
php_response=$(run php '<?php print "Hello from php!\n"; ?>')
py_response=$(run py "print 'Hello from Python!'")

printf "$js_response\n$cs_response\n$php_response\n$py_response\n"

This is just a demo so we want to SEE the results but a response can be used any way you want. We could even do some complicated math in the other language and get the reply and process it further in Bash.

Using stdout and stderr as a "return value"

Bash doesn't have the same sort of return functionality as other languages do. It has it's own set of rules, and if you are to use other languages in Bash you have to play by those rules. For this reason we need to understand them. Take this example:

a_func_or_script () {

  if   [ "$1" = stdout ]; then echo "stdout"      # can be captured
  elif [ "$1" = stderr ]; then  >&2 echo "stderr" # can be captured
  elif [ "$1" = ret_st ]; then return "string"    # ERROR, don't bother
  elif [ "$1" = retn_0 ]; then return 0           # non exiting status code
  elif [ "$1" = exit_0 ]; then exit 0             # exiting status code
  fi
}

echo $(a_func_or_script stdout) # re-echoes echo in function
echo $(a_func_or_script stderr) # re-echoes echo in function
echo $(a_func_or_script retn_0) # echoes nothing, fairly pointless
echo $(a_func_or_script exit_0) # echoes nothing, fairly pointless

stdout_out=$(a_func_or_script stdout) # stores "stdout"
stderr_out=$(a_func_or_script strerr) # stores "stderr"

a_func_or_script retn_0   # no problems, but pointless
# a_func_or_script exit_0 # if not commented, next line never runs!
echo "done"

So we see that we can store what ever the function or script echoes as if it's a return statement in another language by capturing it with var=$(). This would work if printf was used instead or if when were in any other language, using what it uses to print to stdout or stderr.

In Bash return and exit are fairly the same and both emit and integer status (0 is no error, anything else is an error) but exit will actually throw you out of execution unless inside a sub-shell which is what the parenthesis in $() are doing.

Using Status Codes

Status codes are probably less useful for us but one possible use as a sort of boolean value. Bash does not exactly have boolean values. It does have two built-ins: true and false but what they are really doing it returning either 0 or 1, respectively. This is also what test and [ are doing here, returning 0 or 1 to if:

if [ "$1" = "$2" ]; then echo "do something"; fi
if test "$1" = "$2"; then echo "do something"; fi

We could always just use another languages stdout/err and then somehow generate an exit code from it for use in Bash or we could use their status codes directly, providing the language has them. In the first option we still have the issue of storing the code unless we use it in-place (next to if, for example). Here is a Bash function which runs it's argument, discarding the output but forwarding out the status code:

getstat () {
  typeset cmnd="$*"
  typeset ret_code
  eval $cmnd >/dev/null 2>&1
  ret_code=$?
  return $ret_code
}

Here is a similar Bash function which, again, runs it's argument, discarding the output but then string "true" "false" is sent out via stdout, based on what the exit code was

stat2bool () {
  typeset cmnd="$*"
  typeset ret_code
  eval $cmnd >/dev/null 2>&1
  ret_code=$?
  test $ret_code -eq 0 && echo true || echo false
}

I don't wont to sidetrack us too much with status codes because they can get complicated but just know that the first, getstat would work to the right of if in Bash and I'll leave it up to you on how to make it take the output of another language before returning to if. The second, stat2bool, can be be saved to a variable using $() and then that variable will work to the right of if just as if it's the the getstat.

Redirecting Code in Here Documents to Other Language Interpretors

run() is great but it would be nice to have more that one line of code that doesn't have to all be squeezed into one statement. Here is the solution.

var="'Hello from Bash!'"

/usr/bin/env ruby <<EORUBY
  puts $var
  x = 'Hello from Ruby!'
  puts x
EORUBY

This couldn't be more straight-forward. You could even have /usr/bin/env ruby in a Bash variable just to avoid typing it if you find yourself doing it a lot. Note this syntax allows for any content in the code being sent out to be replaced by values of Bash variables, which is very nice. It even does this without the need for double-quotes, which can get very confused and not work if you need double quotes in the actual redirected code!

This doesn't really give you much to work with after it's completed by you might want to try getting the last status code with $? afterward but I haven't tested this.

Writing a Bash Function Using Another Language!

Here it is, a Bash function written in Ruby, in a Bash script!

reduce_fraction_rb () {
  local ruby_code=$(cat <<EOF
  a = $1
  b = $2
  while b != 0 do
    gcd = b
    b = a % b
    a = gcd
  end
  numer = $1 / gcd
  denom = $2 / gcd
  puts "#{numer}/#{denom}"
EOF
)
  echo "$ruby_code" | /usr/bin/env ruby
}

# use it like this:
reduce_fraction_rb 4 6

The function takes it's Bash arguments and interpolates them right into the Ruby code, which is another heredoc. The Bash function runs it. It's up to use to actually use puts in the Ruby code which will act as a sort of return for you later. You can capture the output like this:

result=$(reduce_fraction_rb 4 6)

Bash does not care that the stdout came from Ruby's puts and not from it's own echo or printf, it's all just standard streams!

Here is the same thing with Node.js:

reduce_fraction_js () {
  local js_code=$(cat <<EOF
  var a = $1
  var b = $2;
  while (b !== 0) {
    var gcd = b;
    b = a % b;
    a = gcd;
  }
  numer = $1 / gcd
  denom = $2 / gcd
  console.log (numer + "/" + denom);
EOF
)
  echo "$ruby_code" | /usr/bin/env node
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment