A shell script that can run under different shells
I have a shell script with a syntax compatible to both bash
and zsh
, except for a section that has zsh specific syntax that throws syntax errors if sourced from bash.
Is there an easy way to escape such section when using bash?
The script is a bash function that sources all the files in a directory. It works fine from zsh (and it is irrelevant to the question).
#!/usr/bin/env bash
shell=$(ps -p $$ -oargs=)
if [ $shell = "bash" ]; then
for f in ~/.functions.d/*.sh; do source $f; done
elif [ $shell = "zsh" ]; then
for f (~/.functions.d/**/*.sh) source $f
fi
The error raised when sourcing it in bash
is:
scr: line 8: syntax error near unexpected token `('
scr: line 8: ` for f (~/.functions.d/**/*.sh) source $f'
Relevant links:
- This same question in Stack Overflow.
- Unix & Linux: Source only part of a script from another script?
- Stack Overflow: Using source to include part of a file in a bash script.
2 answers
I'm not familiar with zsh, but it seems to me that your problem here is that the syntax for for
loops is different in bash and zsh, which throws bash off as it tries to interpret your script, finds a keyword it knows but the rest of the statement doesn't match what it expects.
The solution would seem to me to be to put the shell-specific parts in separate files, and only source
the file matching the current execution environment into the script. So you might have a main script like:
#!/usr/bin/env bash
shell=$(ps -p $$ -oargs= | awk '{print $1}')
if [ $shell = "bash" ]; then
source ./bash-source.sh
elif [ $shell = "zsh" ]; then
source ./zsh-source.sh
fi
as well as bash-source.sh
for f in ~/.functions.d/*.sh; do source $f; done
and zsh-source.sh
for f (~/.functions.d/**/*.sh) source $f
This way, each shell only sees the for
statement with the correct syntax for it, and therefore doesn't get tripped up by trying to parse the other. This relies on the fact that at least bash doesn't actually parse a file until it's loaded, and loading happens only when the source
statement is actually executed.
If zsh deals gracefully with this situation, you might not need to split the bash source loop into its own file; but clearly bash doesn't like to see the zsh for
(as evidenced by the fact that you're getting an error relating to it), and thus bash needs a little hand-holding.
Also note that I had to add | awk '{print $1}'
to the $shell
assignment. Without it, when within a bash shell script, $shell
was assigned the whole execution command line; in other words, bash ./filename.sh
even when executing the script only as ./filename.sh
. There's almost certainly a more efficient way to do that; for the moment, I simply needed something that got me past that assignment and would likely give the intended value. This does however suggest some system-dependence in the ps
output that you may need to deal with, depending on just how portable you want this script to be.
I came up with three methods.
Exit early
Use exit
early instead of the elif
syntax.
Basically shell is an interpreter.
As soon as the parsing of the first if
statement is finished, it will be executed.
In this case, if shell is bash, exit
before the parsing of the next if
statement begins.
if [ -n "$BASH_VERSION" ]; then
for f in *; do echo __BASH__ $f; done
exit
fi
if [ -n "$ZSH_VERSION" ]; then
for f (*) echo __ZSH__ $f
fi
Eval string
Use eval
.
if [ -n "$BASH_VERSION" ]; then
for f in *; do echo __BASH__ "$f"; done
elif [ -n "$ZSH_VERSION" ]; then
eval 'for f (*) echo __ZSH__ "$f"'
fi
Source here-document
This is the same as Canina's answer. However, you can write the code in the same file by using here-document.
if [ -n "$BASH_VERSION" ]; then
for f in *; do echo __BASH__ "$f"; done
elif [ -n "$ZSH_VERSION" ]; then
source /dev/stdin << '__ZSH_SRC__'
for f (*) echo __ZSH__ "$f"
__ZSH_SRC__
fi
1 comment thread