shebang env preferred python version

Question:

I have some python-2.x scripts which I copy between different systems, Debian and Arch linux.
Debian install python as ‘/usr/bin/python’ while Arch installs it as ‘/usr/bin/python2’.
A problem is that on Arch linux ‘/usr/bin/python’ also exists which refers to python-3.x.
So every time I copy a file I have to correct the shebang line, which is a bit annoying.

On Arch I use

#!/usr/bin/env python2

While on debian I have

#!/usr/bin/env python

Since ‘python2’ does not exist on Debian, is there a way to pass a preferred application? Maybe with some shell expansion? I don’t mind if it depends on ‘/bin/sh’ existing for example.
The following would be nice but don’t work.

#!/usr/bin/env python2 python
#!/usr/bin/env python{2,}
#!/bin/sh python{2,}
#!/bin/sh -c python{2,}

The frustrating thing is that ‘sh -c python{2,}’ works on the command line: i.e. it calls python2 where available and otherwise python.

I would prefer not to make a make a link ‘python2->python’ on Debian because then if I give the script to someone else it will not run. Neither would I like to make ‘python’ point to python2 on Arch, since it breaks with updates.

Is there a clean way to do this without writing a wrapper?

I realize similar question have been asked before, but I didn’t see any answers meeting my boundary conditions 🙂
Conditional shebang line for different versions of Python

— UPDATE

I hacked together an ugly shell solution, which does the job for now.

#!/bin/bash
pfound=false; v0=2; v1=6
for p in /{usr/,}bin/python*; do  
  v=($(python -V 2>&1 | cut -c 7- | sed 's/./ /g'))
  if [[ ${v[0]} -eq $v0 && ${v[1]} -eq $v1 ]]; then pfound=true; break; fi
done
if ! $pfound; then echo "no suitable python version (2.6.x) found."; exit 1; fi
$p - $* <<EOF

PYTHON SCRIPT GOES HERE

EOF

explanation:
get version number (v is a bash array) and check

v=($(python -V 2>&1 | cut -c 7- | sed 's/./ /g'))
if [[ ${v[0]} -eq $v0 && ${v[1]} -eq $v1 ]]; then pfound=true; break; fi

launch found program $p with input from stdin (-) and pass arguments ($*)

$p - $* <<EOF
...
EOF
Asked By: jaap

||

Answers:

Something like this:

#!/usr/bin/env python
import sys
import os
if sys.version_info >= (3, 0):
    os.execvp("python2.7", ["python2.7", __file__])
    os.execvp("python2.6", ["python2.6", __file__])
    os.execvp("python2", ["python2", __file__])
    print ("No sutable version of Python found")
    exit(2)

Update Below is a more robust version of the same.

#!/bin/bash

ok=bad
for pyth in python python2.7 python2.6 python2; do
  pypath=$(type -P $pyth)
  if [[ -x $pypath ]] ; then
    ok=$(
      $pyth <<@@

import sys 
if sys.version_info < (3, 0):
  print ("ok")
else:
  print("bad")
@@

    )
    if [[ $ok == ok ]] ; then
      break
    fi
  fi
done

if [[ $ok != ok ]]; then
  echo "Could not find suitable python version"
  exit 2
fi

$pyth <<@@
<<< your python script goes here >>>
@@
#!/bin/sh
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
''''which python  >/dev/null 2>&1 && exec python  "$0" "$@" # '''
''''exec echo "Error: I can't find python anywhere"         # '''

import sys
print sys.argv

This is first run as a shell script. You can put almost any shell code in between '''' and # '''. Such code will be executed by the shell. Then, when python runs on the file, python will ignore the lines as they look like triple-quoted strings to python.

The shell script tests if the binary exists in the path with which python2 >/dev/null and then executes it if so (with all arguments in the right place). For more on this, see Why does this snippet with a shebang #!/bin/sh and exec python inside 4 single quotes work?

Note: The line starts with four ' and their must be no space between the fourth ' and the start of the shell command (which…)

Answered By: Aaron McDaid

I’ll leave this here for future reference.

All of my own scripts are usually written for Python 3, so I’m using a modified version of Aaron McDaid’s answer to check for Python 3 instead of 2:

#!/usr/bin/env sh
''''which python3 >/dev/null 2>&1 && exec python3 "$0" "$@" # '''
''''test $(python --version 2>&1 | cut -c 8) -eq 3 && exec python "$0" "$@" # '''
''''exec echo "Python 3 not found." # '''

import sys
print sys.argv
Answered By: reitermarkus

Here is a more concise way for the highest voted answer:

#!/bin/sh
''''exec $(which python3 || which python2 || echo python) $0 $@ #'''

import sys
print(sys.argv)
print(sys.version_info)

You’ll get this if none of them found:

'./test.py: 2: exec: python: not found'

Also, get rid of warnings from linter:

'module level import not at top of file - flake8(E402)'
Answered By: Pamela

In my case I don’t need the python path or other information (like version).

I have an alias setup to point "python" to "python3". But, my scripts are not aware of the environment alias. Therefore I needed a solution for the scripts to determine the program name, programmatically.

If you have more than one version installed, the sort and tail are going to give you the latest version:

#!/bin/sh

command=$((which python3 || which python2 || which python) | sort | tail -n1 | awk -F "/" '{ print $NF }')

echo $command

Would give a result like: python3

This solution is original but similar to @Pamela

Answered By: elvis2
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.