| description | Skill for reviewing shell scripts with shell-neutral behavior with best efforts. |
| name | shell-script-review |
Currently, all the mainstream operating systems are using Bash as their default shell.
However, many programmers like Zsh for its better interactive features.
Below we list several common different behaviors between Bash and Zsh, along with
workarounds to write shell-neutral scripts that work in both shells.
DO NOT over checking!
if [ -f XXX ] then
...
fi
Suspecting everything itself is suspicious: If the file checked is a well committed file in this repo, DO NOT check for it existence at all!
If you are not sure, use git ls-files XXX to check if it is tracked by git.
Similarly for environment variables, check setup.sh and session-init.sh to see if these variables are always set by those scripts.
If so, DO NOT -z or -n check them!
Array Indexing
arr=(apple banana cherry)
echo ${arr[0]}
echo ${arr[1]}
arr=(apple banana cherry)
echo ${arr[1]}
echo ${arr[2]}
Shell-neutral workaround:
Option 1: Force ksh-style arrays in zsh
#!/bin/bash # or #!/bin/zsh
[ -n "$ZSH_VERSION" ] && setopt KSH_ARRAYS
arr=(apple banana cherry)
echo ${arr[0]}
Option 2: Avoid use traversal
for item in "${arr[@]}"; do
echo "$item"
done
Additionally, when parsing positional arguments, extract them to variables directly:
for item in "$@"; do
case $item in
--option)
option_value="$2"
shift 2
;;
*)
positional_args+=("$item")
shift
;;
esac
done
Script Path Detection
echo "$0"
echo "${BASH_SOURCE[0]}"
echo "$0"
echo "${(%):-%x}"
Shell-neutral workaround:
A reliable way to get the script path in both shells:
#!/bin/bash
if [ -n "$BASH_SOURCE" ]; then
SCRIPT_PATH="${BASH_SOURCE[0]}"
elif [ -n "$ZSH_VERSION" ]; then
SCRIPT_PATH="${(%):-%x}"
else
SCRIPT_PATH="$0"
fi
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
echo "Script location: $SCRIPT_DIR"
Another option is to reply on environment variables exported by setup.sh and we use
absolute paths based on those or absolute paths by git rev-parse.
Variable Expansion & Word Splitting
var="one two three"
echo $var
echo "$var"
for word in $var; do echo "$word"; done
var="one two three"
echo $var
echo "$var"
for word in $var; do echo "$word"; done
for word in ${=var}; do echo "$word"; done
Shell-neutral workaround:
#!/bin/bash
var="one two three"
echo "$var"
read -ra words <<< "$var"
for word in "${words[@]}"; do
echo "$word"
done
Globbing
shopt -s globstar
echo **/*.txt
echo *.txt
echo **/*.txt
echo **/*.txt~*test*
echo *.txt(.)
Shell-neutral workaround:
#!/bin/bash
find . -name "*.txt" -type f
if [ -n "$BASH_VERSION" ]; then
shopt -s globstar
fi
echo **/*.txt
echo *.txt
Arrays & Associative Arrays
Bash:
arr=(a b c)
echo ${arr[0]}
declare -A map
map[key1]="value1"
map[key2]="value2"
echo ${map[key1]}
Zsh:
arr=(a b c)
echo ${arr[1]}
typeset -A map
map=(key1 value1 key2 value2)
map[key1]="value1"
echo ${map[key1]}
Shell-neutral workaround:
#!/bin/bash
[ -n "$ZSH_VERSION" ] && setopt KSH_ARRAYS
arr=(a b c)
echo ${arr[0]}
declare -A map 2>/dev/null || typeset -A map
map[key1]="value1"
echo ${map[key1]}
```zsh
local path="screwed"
echo $PATH
In zsh, $path is an array view of the PATH variable, which can lead to confusion.
In bash, $path is just a regular variable.
Solution: Always use $PATH for environment variable access, and avoid using $path as variable name!
Key Recommendations
- Use
#!/bin/bash as shebang (more portable)
- Add setopt KSH_ARRAYS at the top if you must support zsh
- Always quote variables:
"$var" not $var
- Use
"${arr[@]}" for array expansion
- Use the script path template for reliable path detection
- Use find instead of complex globs for portability
- Avoid using
$path as in zsh, it is different view of the sameting, $PATH