Loop over a string in zsh and Bash

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.


if type emulate >/dev/null 2>/dev/null; then emulate ksh; fi

In zsh, this activates options that make it more compatible with ksh and bash, including sh_word_split. In other shells, emulate doesn't exist so this does nothing.


If you are not afraid to use eval (= evil):

x="one two three"
eval "x=($x)"
for i in ${x[@]}; do 
    echo $i
done