Ruby/Shell Polyglot Scripts

2023-11-13

I wanted to write a script that could be executed by either /bin/sh or Ruby and successfully work in either case. I couldn’t find a working example on the Internet or via GPT-4, so ended up figuring this one out:

print () {
=begin
}
echo 'running as shell'
exit
=end
}
puts 'running as ruby'

This is slightly arcane so let’s walk through it from both perspectives.

What /bin/sh sees

First, we’re defining a function called print that takes no arguments. If we would ever run it (we won’t), it would try to find an execute a program named =begin. Then, it just runs echo 'running as shell' and exits. Simple! The stuff afterward is ignored because /bin/sh doesn’t prospectively parse ahead of the current statement.

What Ruby sees

This is a little weirder. First, we’re calling print with no arguments, and a block. print accepts a block, and as far as I can tell just does nothing with it. When you call it with no arguments, it just does nothing. So essentially this print call is just a no-op.

=begin and =end are a very rarely used ruby feature that demarcates block comments. So the whole shell bit in the middle is a comment from ruby’s perspective. Let’s highlight this as ruby to demonstrate:

print () {
=begin
}
echo 'running as shell'
exit
=end
}
puts 'running as ruby'

That makes things more clear. We’re just calling print with no arguments and a block (which gets ignored) containing a block comment. Then we run our ruby code that gets ignored by the shell.