How can I have more than one possibility in a script’s shebang line?

I’m in a bit of an interesting situation where I have a Python script that can theoretically be run by a variety of users with a variety of environments (and PATHs) and on a variety of Linux systems. I want this script to be executable on as many of these as possible without artificial restrictions. Here are some known setups:

  • Python 2.6 is the system Python version, so python, python2, and python2.6 all exist in /usr/bin (and are equivalent).
  • Python 2.6 is the system Python version, as above, but Python 2.7 is installed alongside it as python2.7.
  • Python 2.4 is the system Python version, which my script does not support. In /usr/bin we have python, python2, and python2.4 which are equivalent, and python2.5, which the script supports.

I want to run the same executable python script on all three of these. It would be nice if it tried to use /usr/bin/python2.7 first, if it exists, then fall back to /usr/bin/python2.6, then fall back to /usr/bin/python2.5, then simply error out if none of those were present. I’m not too hung up on it using the most recent 2.x possible, though, as long as it’s able to find one of the correct interpreters if present.

My first inclination was to change the shebang line from:

#!/usr/bin/python

to

#!/usr/bin/python2.[5-7]

since this works fine in bash. But running the script gives:

/usr/bin/python2.[5-7]: bad interpreter: No such file or directory

Okay, so I try the following, which also works in bash:

#!/bin/bash -c /usr/bin/python2.[5-7]

But again, this fails with:

/bin/bash: - : invalid option

Okay, obviously I could just write a separate shell script that finds the correct interpreter and runs the python script using whatever interpreter it found. I’d just find it a hassle to distribute two files where one should suffice as long as it’s run with the most up-to-date python 2 interpreter installed. Asking people to invoke the interpreter explicitly (e.g., $ python2.5 script.py) is not an option. Relying on the user’s PATH being set up a certain way is also not an option.

Edit:

Version checking within the Python script is not going to work since I’m using the “with” statement which exists as of Python 2.6 (and can be used in 2.5 with from __future__ import with_statement). This causes the script to fail immediately with a user-unfriendly SyntaxError, and prevents me from ever having an opportunity to check the version first and emit an appropriate error.

Example: (try this with a Python interpreter less than 2.6)

#!/usr/bin/env python

import sys

print "You'll never see this!"
sys.exit()

with open('/dev/null', 'w') as out:
    out.write('something')

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

I’m not expert, but I believe that you shouldn’t specify exact python version to use and leave that choice to system/user.

Also you should use that instead of hardcoding path to python in script:

#!/usr/bin/env python

or

#!/usr/bin/env python3 (or python2)

It is recommended by Python doc in all versions:

A good choice is usually

#!/usr/bin/env python

which searches for the Python interpreter in the whole PATH. However,
some Unices may not have the env command, so you may need to hardcode
/usr/bin/python as the interpreter path.

In various distributions Python may be installed in different places, so env will search for it in PATH. It should be available in all major Linux distributions and from what I see in FreeBSD.

Script should be executed with that version of Python which is in your PATH and which is chosen by your distribution*.

If your script is compatible with all version of Python except 2.4 you should just check inside of it if it is run in Python 2.4 and print some info and exit.

More to read

  • Here you can find examples in what places Python might be installed in different systems.
  • Here you can find some advantages and disadvantages for using env.
  • Here you can find examples of PATH manipulation and different results.

Footnote

*In Gentoo there is tool called eselect. Using it you may set default versions of different applications (including Python) as default:

$ eselect python list
Available Python interpreters:
  [1]   python2.6
  [2]   python2.7 *
  [3]   python3.2
$ sudo eselect python set 1
$ eselect python list
Available Python interpreters:
  [1]   python2.6 *
  [2]   python2.7
  [3]   python3.2

Method 2

Based on some ideas from a few comments, I managed to cobble together a truly ugly hack that seems to work. The script becomes a bash script wraps a Python script and passes it to a Python interpreter via a “here document”.

At the beginning:

#!/bin/bash

''':'
vers=( /usr/bin/python2.[5-7] )
latest="${vers[$((${#vers[@]} - 1))]}"
if !(ls $latest &>/dev/null); then
    echo "ERROR: Python versions < 2.5 not supported"
    exit 1
fi
cat <<'# EOF' | exec $latest - "[email protected]"
''' #'''

The Python code goes here. Then at the very end:

# EOF

When the user runs the script, the most recent Python version between 2.5 and 2.7 is used to interpret the rest of the script as a here document.

An explanation on some shenanigans:

The triple-quote stuff I’ve added also allows this same script to be imported as a Python module (which I use for testing purposes). When imported by Python, everything between the first and second triple-single-quote is interpreted as a module-level string, and the third triple-single-quote is commented out. The rest is ordinary Python.

When run directly (as a bash script now), the first two single-quotes become an empty string, and the third single-quote forms another string with the fourth single-quote, containing only a colon. This string is interpreted by Bash as a no-op. Everything else is Bash syntax for globbing the Python binaries in /usr/bin, selecting the last one, and running exec, passing the rest of the file as a here document. The here document starts with a Python triple-single-quote containing only a hash/pound/octothorpe sign. The rest of the script then is interpreted as normal until the line reading ‘# EOF’ terminates the here document.

I feel like this is perverse, so I hope somebody has a better solution.

Method 3

The shebang line can only specify a fixed path to an interpreter. There’s the #!/usr/bin/env trick to look up the interpreter in the PATH but that’s it. If you want more sophistication, you’ll need to write some wrapper shell code.

The most obvious solution is to write a wrapper script. Call the python script foo.real and make a wrapper script foo:

#!/bin/sh
if type python2 >/dev/null 2>/dev/null; then
  exec python2 "$0.real" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="8eaace">[email protected]</a>"
else
  exec python "$0.real" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="082c48">[email protected]</a>"
fi

If you want to put everything in one file, you can often make it a polyglot that starts with a #!/bin/sh line (so will be executed by the shell) but is also a valid script in another language. Depending on the language, a polyglot may be impossible (if #! causes a syntax error, for example). In Python, it isn’t very difficult.

#!/bin/sh
''':'
if type python2 >/dev/null 2>/dev/null; then
  exec python2 "$0.real" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9bbfdb">[email protected]</a>"
else
  exec python "$0.real" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="d6f296">[email protected]</a>"
fi
'''
# real Python script starts here
def …

(The whole text between ''' and ''' is a Python string at toplevel, which has no effect. For the shell, the second line is ''':' which after stripping quotes is the no-op command :.)

Method 4

As your requirements state a known list of binaries, you could do it in Python with the following. It wouldn’t work past a single digit minor/major version of Python but I don’t see that happening any time soon.

Runs the highest version located on disk from the ordered, increasing list of versioned pythons, if the version tagged on the binary is higher than the current version of python executing. The “ordered increasing list of versions” being the important bit for this code.

#!/usr/bin/env python
import os, sys

pythons = [ '/usr/bin/python2.3','/usr/bin/python2.4', '/usr/bin/python2.5', '/usr/bin/python2.6', '/usr/bin/python2.7' ]
py = list(filter( os.path.isfile, pythons ))
if py:
  py = py.pop()
  thepy = int( py[-3:-2] + py[-1:] )
  mypy  = int( ''.join( map(str, sys.version_info[0:2]) ) )
  if thepy > mypy:
    print("moving versions to "+py)
    args = sys.argv
    args.insert( 0, sys.argv[0] )
    os.execv( py, args )

print("do normal stuff")

Apologies for my scratchy python

Method 5

You can write a small bash script which checks for the available phython executable and calls it with the script as parameter. You can then make this script the shebang line target:

#!/my/python/search/script

And this script simply does (after the search):

"$python_path" "$1"

I was unsure whether the kernel would accept this script indirection but I checked and it works.

Edit 1

To make this embarrassing unperceptiveness a good proposal finally:

It is possible to combine both scripts in one file. You just write the python script as a here document in the bash script (if you change the python script you just need to copy the scripts together again). Either you create a temporary file in e.g. /tmp or (if python supports that, I don’t know) you provide the script as input to the interpreter:

# do the search here and then
# either
cat >"tmpfile" <<"EOF" # quoting EOF is important so that bash leaves the python code alone
# here is the python script
EOF
"$python_path" "tmpfile"
# or
"$python_path" <<"EOF"
# here is the python script
EOF


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

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x