Is there a way to loop over $x in zsh when it is a string and in a way compatible with Bash?
Yes!. A var expansion is not split (by default) in zsh, but command expansions are. Therefore in both Bash and zsh you can use:
x="one two three"
for i in $( echo "$x" )
do
echo "$i"
done
In fact, the code above works the same in all Bourne shell descendants (but not in the original Bourne, change $(…) to `…` to get it working there).
The code above still have some issues with globbing and the use of echo, keep reading.
In zsh, a var expansion like $var is not split (also not glob) by default.
This code has no problems (doesn't expand to all files in the pwd) in zsh:
var="one * two"
printf "<%s> " ${var}; echo
But also doesn't split var by the value of IFS.
For zsh, splitting on IFS could be done by either using:
1. Call splitting explicitly: `echo ${=var}`.
2. Set SH_WORD_SPLIT option: `set -y; echo ${var}`.
3. Using read to split to a list of vars (or an array with `-A`).
But none of those options are portable to bash (or any other shell except ksh for -A).
Going down to an older syntax that is shared by both shells: read might help.
But that can only work for one character delimiters (not IFS), and only if the delimiter exists in the input string:
# ksh, zsh and bash(3.0+)
t1(){ set -f;
while read -rd "$delimiter" i; do
echo "|$i|"
done <<<"$x"
}
Where $1 is a one character delimiter.
That still suffer of the expansion of globbing characters (*, ? and [), so a set -f is required. And, we can set an array variable outarr instead:
# for either zsh or ksh
t2(){ set -f; IFS="$delimiter" read -d $'\3' -A outarr < <(printf '%s\3' "$x"); }
And the same idea for bash:
# bash
t3(){ local -; set -f; mapfile -td "$1" outarr < <(printf '%s' "$x"); }
The effect of set -f is restored in the bash function by using local -.
The concept could even be extended to a limited shell like dash:
# valid for dash and bash
t4(){ local -; set -f;
while read -r i; do
printf '%s' "$i" | tr "$delimiter"'\n' '\n'"$delimiter"; echo
done <<-EOT
$(echo "$x" | tr "$delimiter"'\n' '\n'"$delimiter")
EOT
}
No <<<, no <(…) and no read -A or readarray used, but it works (for one character delimiters) with spaces, newlines, and/or control characters in the input.
But it is a lot easier to simply do:
t9(){ set -f; outarr=( $(printf '%s' "$x") ); }
Sadly, zsh doesn't understand the local -, so the value of set -f has to be restored as this:
t99(){ oldset=$(set +o); set -f; outarr=( $( printf '%s' "$x" ) ); eval "$oldset"; }
Any of the functions above may be called with:
IFS=$1 delimiter=$1 $2
Where the first argument $ is the delimiter (and IFS) and the second argument is the function to call (t1, t2, … t9, t99). That call sets the value of IFS only for the duration of the function call which gets restored to its original value when the function called exits.