1

Let's suppose that you have a super python 2&3 one-liner that you need to use in a shell script.

On system A the python command works, but on system B you have to use python3, on system C you need to use python3.12 or python2, etc...

What would be a sensible way to check some, if not every, "python*" command name?

I want to avoid using a construct like this:

#!/bin/bash if command -v python then python_exe=python elif command -v python3 then python_exe=python3 elif command -v python2 then python_exe=python2 else echo "python executable not found" >&2 exit 1 fi > /dev/null "$python_exe" -c 'print("Hello world!");' 

    3 Answers 3

    2

    If switching to zsh is an option

    #! /bin/zsh - die() { print -ru2 -- "$@"; exit 1; } python_cmd=( ${(MOnk)commands:#python(|<2-3>(|.<->))} ) (( $#python_cmd )) || die No python hash python=$commands[$python_cmd[1]] python -c 'print("Hello world!")' 

    In zsh, $commands is a special associative array that maps commands (those found in $path) to their path (an interface to the command hash table as managed by the standard hash builtin utility).

    • ${(k)commands} expands to the keys of that associative array.
    • With ${(M)array:#pattern}, that's restricted to the ones that Match the pattern (here python optionally followed by 2 or 3 and optional .subversion).
    • With n and O that's Ordered numerically in reverse so newer versions are preferred.

    You can change the last line to exec python "$@" to make that script a python wrapper that runs the latest python 2 or 3 with the supplied arguments (and python as argv[0]), though beware that not all systems support using scripts as she-bang so using #! /path/to/that/script may not work on all.

    1
    • That's good to know, thankks; it's no surprise for zsh to have some builtin facilities that simplifies the task. Still, knowing how to do it in bash would be great.
      – Fravadona
      CommentedMar 12 at 10:12
    1

    Here's a variant for bash

    python=$( cmd=python readarray -t dirs < <(tr : $'\n' <<<"$PATH") for dir in "${dirs[@]}" do for file in "$dir/$cmd"* do [ -x "$file" ] && echo "${file##*/}" done done | grep -E "^$cmd[0-9.-]*$" | sort -Vr | head -n1 ) "$python" ... 
    2
    • Thank you, that inspired me for the POSIX solution I was writing
      – Fravadona
      CommentedMar 13 at 8:24
    • @Fravadona excellent :-)CommentedMar 13 at 11:11
    1

    BASH Solution

    In bash there's a compgen builtin that is able to "complete" a command (i.e. its trailing part). Its output is a LF-delimited list of the possible completions, and its return status is falsey when no match is found.

    Here's an example of how you could use it to pick up the first available "python" command:

    edit: I added the grep filtering as mentioned in @StéphaneChazelas comment below.

    #!/bin/bash if IFS= read -r python_exe then "$python_exe" -c 'print("Hello world!")' else echo 'python interpreter not found' >&2 exit 1 fi < <( compgen -c | grep -xE 'python([23](\.[0123456789]+)?)?' ) 

    POSIX Solution

    In a standard shell it's possible to write a function that mimics compgen -c (at least for the commands that are in the PATH) and then use it to get the "python" executable name:

    #!/bin/sh compcom() ( case $1 in ( */* ) for f in "$1"*; do [ -f "$f" ] && [ -x "$f" ] && printf '%s\n' "$f"; done ;; ( * ) p=$PATH while [ -n "$p" ]; do d=${p%%:*} p=${p#"$d"} p=${p#:} [ -d "$d" ] || continue for f in "${d%/}/$1"*; do [ -f "$f" ] && [ -x "$f" ] && printf '%s\n' "${f##*/}"; done done ;; esac ) python_exe=$( compcom | grep -xE 'python([23](\.[0123456789]+)?)?' | head -n 1 ) [ -n "$python_exe" ] || { echo 'python interpreter not found' >&2 exit 1 } "$python_exe" -c 'print("Hello world!")' 
    6
    • 1
      That returns things that are not python interpreters though such as python-config, python3-pasteurize. Maybe better as compgen -c python | grep -xE 'python([23](\.[01234567890]+)?)?' | sort -Vru assuming GNU sort or compatible.CommentedMar 12 at 11:50
    • Nice catch, a grep seems mandatory indeed. And if grep is used then there's no point in specifying python as last argument of compgen, as compgen -c | grep ... is enough. On the other hand, the sort would allow selecting the latest python command, but I can't really count on GNU sort on the target systems, and it doesn't really matter which version of python is used in my use-case either.
      – Fravadona
      CommentedMar 12 at 12:29
    • 1
      @Fravadona "I can't really count on GNU sort on the target systems" - you will probably find that LC_ALL=C sort -r is sufficientCommentedMar 13 at 11:22
    • @ChrisDavies That's right, and as long as there's no python10 then the standard sort should be enough for selecting between main versions. Tangentially, the core of my answer is in fact the compgen -c command (which I found in the bash-completion scripts) and porting it to POSIX, FWIW. The command you pick from the output depends on the use-case.
      – Fravadona
      CommentedMar 13 at 11:59
    • Empirically when testing, the more relaxed RE in my answer ("^$cmd[0-9.-]*$") worked well for many commandsCommentedMar 13 at 12:07

    You must log in to answer this question.

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.