Is there any way to dynamically choose the interpreter that’s executing a script? I have a script that I’m running on two different systems, and the interpreter I want to use is located in different locations on the two systems. What I end up having to to is change the hashbang line every time I switch over. I would like to do something that is the logical equivalent of this (I realize that this exact construct is impossible):
if running on system A:
#!/path/to/python/on/systemA
elif running on system B:
#!/path/on/systemB
#Rest of script goes here
Or even better would be this, so that it tries to use the first interpreter, and if it doesn’t find it uses the second:
try:
#!/path/to/python/on/systemA
except:
#!path/on/systemB
#Rest of script goes here
Obviously, I can instead execute it as
/path/to/python/on/systemA myscript.py
or
/path/on/systemB myscript.py
depending on where I am, but I actually have a wrapper script that launches myscript.py, so I would like to specify the path to the python interpreter programmatically rather than by hand.
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
You can always make a wrapper script to find the correct interpreter for the actual program:
#!/bin/bash
if something ; then
interpreter=this
script=/some/path/to/program.real
flags=()
else
interpreter=that
script=/other/path/to/program.real
flags=(-x -y)
fi
exec "$interpreter" "${flags[@]}" "$script" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e6c2a6">[email protected]</a>"
Save the wrapper in the users’ PATH as program and put the actual program aside or with another name.
I used #!/bin/bash in the hashbang because of the flags array. If you don’t need to store a variable number of flags or such and can do without it, the script should work portably with #!/bin/sh.
Method 2
No, that won’t work. The two characters #! absolutely needs to be the first two characters in the file (how would you specify what interpreted the if-statement anyway?). This constitutes the “magic number” that the exec() family of functions detects when they determine whether a file that they are about to execute is a script (which needs an interpreter) or a binary file (which doesn’t).
The format of the shebang line is quite strict. It needs to have an absolute path to an interpreter and at most one argument to it.
What you can do is to use env:
#!/usr/bin/env interpreter
Now, the path to env is usually /usr/bin/env, but technically that’s no guarantee.
This allows you to adjust the PATH environment variable on each system so that interpreter (be it bash, python or perl or whatever you have) is found.
A downside with this approach is that it will be impossible to portably pass an argument to the interpreter.
This means that
#!/usr/bin/env awk -f
and
#!/usr/bin/env sed -f
is unlikely to work on some systems.
Another obvious approach is to use GNU autotools (or some simpler templating system) to find the interpreter and place the correct path into the file in a ./configure step, which would be run upon installing the script on each system.
One could also resort to running the script with an explicit interpreter, but that’s obviously what you’re trying to avoid:
$ sed -f script.sed
Method 3
You can also write a polyglot (combine two languages). /bin/sh is guaranteed to exist.
This has the downside of ugly code and perhaps some /bin/shs could potentially get confused. But it can be used when env does not exist or exists somewhere else than /usr/bin/env. It can also be used if you want to do some pretty fancy selection.
The first part of the script determines which interpreter to use when run with /bin/sh as interpreter, but is ignored when run by the correct interpreter. Use exec to prevent the shell from running more than the first part.
Python example:
#!/bin/sh
'''
' 2>/dev/null
# Python thinks this is a string, docstring unfortunately.
# The shell has just tried running the <newline> program.
find_best_python ()
{
for candidate in pypy3 pypy python3 python; do
if [ -n "$(which $candidate)" ]; then
echo $candidate
return
fi
done
echo "Can't find any Python" >/dev/stderr
exit 1
}
interpreter="$(find_best_python)" # Replace with something fancier.
# Run the rest of the script
exec "$interpreter" "$0" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f7d3b7">[email protected]</a>"
'''
Method 4
I prefer Kusalananda’s and ilkkachu’s answers, but here is an alternative answer that more directly does what the question was asking for, simply because it was asked.
#!/usr/bin/ruby -e exec "non-existing-interpreter", ARGV[0] rescue exec "python", ARGV[0]
if True:
print("hello world!")
Note that you can only do this when the interpreter permits writing code in the first argument. Here, -e and everything after it is taken verbatim as 1 argument to ruby. As far as I can tell, you can’t use bash for the shebang code, because bash -c requires the code to be in a separate argument.
I tried doing the same with python for shebang code:
#!/usr/bin/python -cexec("import sys,osntry: os.execlp('non-existing-interpreter', 'non-existing-interpreter', sys.argv[1])nexcept: os.execlp('ruby', 'ruby', sys.argv[1])")
if true
puts "hello world!"
end
but it turns out too long and linux (at least on my machine) truncates the shebang to 127 characters. Please excuse the use of exec to insert newlines as python doesn’t permit try-excepts or imports without newlines.
I’m not sure how portable this is, and I wouldn’t do it on code meant to be distributed. Nevertheless, it’s doable. Maybe someone will find it useful for quick-and-dirty debugging or something.
Method 5
While this doesn’t select the interpreter within the shell script (it selects it per machine) it is an easier alternative if you have administrative access to all the machines you are trying to run the script on.
Create a symlink (or a hardlink if desired) to point to the desired interpreter path. For example, on my system perl and python are in /usr/bin:
cd /bin ln -s /usr/bin/perl perl ln -s /usr/bin/python python
would create a symlink to allow the hashbang to resolve for /bin/perl, etc. This preserves the ability to pass parameters to the scripts as well.
Method 6
I was confronted with a similar problem like this today (python3 pointing to a version of python that was too old on one system), and came up with an approach that is a bit different from the ones discussed here: Use the “wrong” version of python to bootstrap into the “right” one. The limitation is that some version of python needs to be reachable reliably, but that can usually be achieved by e.g. #!/usr/bin/env python3.
So what I do is start my script with:
#!/usr/bin/env python3
import sys
import os
# On one of our systems, python3 is pointing to python3.3
# which is too old for our purposes. 'Upgrade' if needed
if sys.version_info[1] < 4:
for py_version in ['python3.7', 'python3.6', 'python3.5', 'python3.4']:
try:
os.execlp(py_version, py_version, *sys.argv)
except:
pass # Deliberately ignore errors, pick first available version
What this does is:
- Check the interpreter version for some acceptance criterium
- If not acceptable, go through a list of candidate versions, and re-execute itself with the first of these that’s available
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0