Scripts: bash or zsh
With macOS Catalina, Apple made zsh
the default shell. Pundits saw this as
a sign that the bash will eventually be dropped from macOS,
which of course is not good news for bash coders.
The zsh is very, but not entirely, compatible with bash. If you restrict yourself to a common subset of both shells, namely Bourne shell capabilities, and add some glue code for a few needed differences, it is possible to run the identical script with either bash or zsh.
But what to use as the shebang now. #!/usr/bin/env bash
or
#!/usr/bin/env zsh
?
Return to #!/bin/sh
I’d like to give preference to the bash and use the zsh as a fallback, unless I know that the zsh will give me much better performance. So this is the (default) decision matrix:
bash installed | zsh installed | shell environment |
---|---|---|
no | no | fail |
no | yes | zsh |
no | as sh | zsh |
yes | no | bash |
yes | yes | bash |
yes | as sh | zsh |
as sh | no | bash |
as sh | yes | bash |
The solution I came up with, is the following code taken from mulle-boot:
#! /bin/sh
#
# Prelude to be placed at top of each script. Rerun this script either in
# bash or zsh, if not already running in either (which can happen!)
# Allows script to run on systems that either have bash (linux) or
# zsh (macOS) only
#
if [ "$1" != --no-auto-shell ]
then
if [ -z "${BASH_VERSION}" -a -z "${ZSH_VERSION}" ]
then
exe_shell="`command -v "bash" `"
exe_shell="${exe_shell:-`command -v "zsh" `}"
script="$0"
#
# Quote incoming arguments for shell expansion
#
args=""
for arg in "$@"
do
# True Bourne sh doesn't know ${a//b/c} and <<<
case "${arg}" in
*\'*)
# Use cat instead of echo to avoid possible echo -n
# problems. Escape single quotes in string.
arg="`cat <<EOF | sed -e s/\'/\'\\\"\'\\\"\'/g
${arg}
EOF
`"
;;
esac
if [ -z "${args}" ]
then
args="'${arg}'"
else
args="${args} '${arg}'"
fi
done
#
# bash/zsh will use arg after -c <arg> as $0, convenient!
#
exec "${exe_shell:-bash}" -c ". ${script} --no-auto-shell ${args}" "${script}"
fi
else
shift # get rid of --no-auto-shell
fi
Let’s say the script is called foo
and was invoked with foo "a b" "'c d'"
,
just to make it hard.
First up it’s checked that the first parameter is --no-auto-shell
. It’s
expected that no-one will pass in --no-auto-shell
as a useful parameter
to the actual script.
In case the currently executing shell is already the bash or zsh (checked with the version tests), then the script just bypasses everything and the script continues. This happens in real life on the first run. And it always should happen on the second run.
If we are actually in the Bourne shell, we now let either the bash or
the zsh re-evaluate the whole script. The --no-auto-shell
gets prepended to
the arguments to signify that a “boot” has happened. It is also necessary to
quote protect the arguments from evaluation by the shell. That is as tricky as
is shown in sh.
Now the script runs in a platform independent manner either in zsh or bash.
Note
Technically the
--no-auto-shell
shouldn’t be necessary as either of the two shell version values should be defined on the second run. But in case they aren’t, we aren’t looping endlessly.