38

How do I modify the following code so that when run in zsh it expands $things and iterates through them one at a time?

things="one two"

for one_thing in $things; do
    echo $one_thing
done

I want the output to be:

one 
two

But as written above, it outputs:

one two

(I'm looking for the behavior that you get when running the above code in bash)

oguz ismail
  • 1
  • 16
  • 47
  • 69
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
  • 4
    Does that mean that `zsh` is not word-splitting `$things` when it is the list of a `for` loop? Is it entering the body of the loop just once? Answer, after fighting through the configuration script, is "Yes". Scratch `zsh` from the list of usable shells; that's just too non-POSIX-shell like. I can't even be bothered to start thinking about how it might be configurable to work 'normally'. – Jonathan Leffler Apr 18 '14 at 16:01
  • 1
    You might not like it but that behaviour is completely POSIX compliant, and as far as I know is the expected behaviour for the sh shell. – SantiMar Jul 31 '20 at 03:49

4 Answers4

65

In order to see the behavior compatible with Bourne shell, you'd need to set the option SH_WORD_SPLIT:

setopt shwordsplit      # this can be unset by saying: unsetopt shwordsplit
things="one two"

for one_thing in $things; do
    echo $one_thing
done

would produce:

one
two

However, it's recommended to use an array for producing word splitting, e.g.,

things=(one two)

for one_thing in $things; do
    echo $one_thing
done

You may also want to refer to:

3.1: Why does $var where var="foo bar" not do what I expect?

Rob Bednark
  • 25,981
  • 23
  • 80
  • 125
devnull
  • 118,548
  • 33
  • 236
  • 227
  • 1
    Rather than set `shwordsplit` globally, you can enable `sh`-style word splitting (using `IFS`, of course) for a single parameter with `${=things}`. – chepner Apr 18 '14 at 16:43
  • 1
    @chepner, what would that look like? I tried `IFS=' '; for one_thing in ${=things}; do` but got a `bad substitution` error – Rob Bednark Apr 18 '14 at 17:53
  • @RobBednark Why don't you use an array? – devnull Apr 18 '14 at 17:56
  • 2
    It should just be `for one_thing in ${=things}; do`. What version of `zsh` are you using? Works for me in 4.3.10. The array is a better idea, though, since you don't have to rely on using a string delimiter that isn't present in any of the individual items. – chepner Apr 18 '14 at 18:02
  • @chepner, I'm using zsh 5.0.2 (x86_64-apple-darwin13.0) – Rob Bednark Apr 19 '14 at 00:15
  • Someone zsh guru may very well explain to the rest of us why this practice is there at all. I mean, how can it be default to _not_ split into substrings in a loop? – gustafbstrom Jan 08 '19 at 12:52
  • setopt shwordsplit does not seem to work with zsh 5.3.1., nor {=things}. Only IFS=" " did the trick, and ${(z)things} as stated in the other answer. – pawamoy May 30 '19 at 23:39
  • Are arrays POSIX compliant? I thought they weren't. – SantiMar Jul 31 '20 at 03:50
20

Another way, which is also portable between Bourne shells (sh, bash, zsh, etc.):

things="one two"

for one_thing in $(echo $things); do
    echo $one_thing
done

Or, if you don't need $things defined as a variable:

for one_thing in one two; do
    echo $one_thing
done

Using for x in y z will instruct the shell to loop through a list of words, y, z.

The first example uses command substitution to transform the string "one two" into a list of words, one two (no quotes).

The second example is the same thing without echo.

Here's an example that doesn't work, to understand it better:

for one_thing in "one two"; do
    echo $one_thing
done

Notice the quotes. This will simply print

one two

because the quotes mean the list has a single item, one two.

bgdnlp
  • 1,031
  • 9
  • 12
9

You can use the z variable expansion flag to do word splitting on a variable

things="one two"

for one_thing in ${(z)things}; do
    echo $one_thing
done

Read more about this and other variable flags in man zshexpn, under "Parameter Expansion Flags."

Kevin
  • 53,822
  • 15
  • 101
  • 132
3

You can assume the Internal Field Separator (IFS) on bash to be \x20 (space). This makes the following work:

#IFS=$'\x20'
#things=(one two) #array
things="one two"  #string version

for thing in ${things[@]}
do
   echo $thing
done

With this in mind you can implement this in many different ways just manipulating the IFS; even on multi-line strings.

Atttacat
  • 383
  • 3
  • 4