I would like to convert this Bash loop:

x="one two three"

for i in ${x}
    echo ${i}

in such a way to work with both Bash and zsh

This solution works:

x=( one two three )

for i in ${x[@]}
    echo ${i}

Anyway I am modifying x from a string to an array.

Is there a way to loop over $x in zsh when it is a string and in a way compatible with Bash?

I know about zsh setopt shwordsplit to emulate Bash, but I can't set it ad hoc for the loop, because it would not work in 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" )
    echo "$i"

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")

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.

  • This should be the accepted answer.
    – vfxdev
    Aug 26 '21 at 11:19
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
  • the "simpler" solution is not simpler at all; at a contrary, it will fork an extra process (subshell) in bash. The only advantage is that it will be able to handle ;, |, (, etc but not * in the x variable. Your initial solution (using eval) was just better.
    – mosvy
    Dec 29 '18 at 15:15
  • How is the (new) first part of this answer better than the answer I already posted?
    – IsaaC
    Dec 29 '18 at 15:18
  • @mosvy (1)The initial answer (as the second answer) is affected by globbing. That is easy to avoid (correctly) by doing set -f. (2) But "the original answer" (with eval) is also affected by a second expansion step which is very difficult to resolve (I would say: impossible to solve). (3) You are missing the point that the $(echo "$x") is perfectly portable to all bourne like shells. (4) If you want solutions that correctly process any input including spaces, newlines and/or control characters, then: read my answer.
    – IsaaC
    Dec 29 '18 at 15:30
  • @Isaac Oops, sorry Isaac, I actually realized this different method without reading your answer that has the exact same method. You have to believe me on this one. Anyway, I upvoted your answer and it has now more upvotes than me now, which I think is deserved. Also rollbacked the edit. Dec 29 '18 at 19:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.