Shell – Path Independent Shebangs Explained

cronpathshellzsh

I have a script that I want to be able to run in two machines. These two machines get copies of the script from the same git repository. The script needs to run with the right interpreter (e.g. zsh).

Unfortunately, both env and zsh live in different locations in the local and remote machines:

Remote machine

$ which env
/bin/env

$ which zsh
/some/long/path/to/the/right/zsh

Local machine

$ which env
/usr/bin/env

$which zsh
/usr/local/bin/zsh

How can I set up the shebang so that running the script as /path/to/script.sh always uses the Zsh available in PATH?

Best Answer

You cannot solve this through shebang directly, since shebang is purely static. What you could do is having some »least common multiplier« (from a shell perspective) in the shebang and re-execute your script with the right shell, if this LCM isn't zsh. In other words: Have your script executed by a shell found on all systems, test for a zsh-only feature and if the test turns out false, have the script exec with zsh, where the test will succeed and you just continue.

One unique feature in zsh, for example, is the presence of the $ZSH_VERSION variable:

#!/bin/sh -

[ -z "$ZSH_VERSION" ] && exec zsh - "$0" ${1+"$@"}

# zsh-specific stuff following here
echo "$ZSH_VERSION"

In this simple case, the script is first executed by /bin/sh (all post-80s Unix-like systems understand #! and have a /bin/sh , either Bourne or POSIX but our syntax is compatible to both). If $ZSH_VERSION is not set, the script exec's itself through zsh. If $ZSH_VERSION is set (resp. the script already is run through zsh), the test is simply skipped. Voilà.

This only fails if zsh isn't in the $PATH at all.

Edit: To make sure, you only exec a zsh in the usual places, you could use something like

for sh in /bin/zsh \
          /usr/bin/zsh \
          /usr/local/bin/zsh; do
    [ -x "$sh" ] && exec "$sh" - "$0" ${1+"$@"}
done

This could save you from accidentally exec'ing something in your $PATH which is not the zsh you're expecting.

Related Question