100

I have noticed there are two alternative ways of building loops in zsh:

  1. for x (1 2 3); do echo $x; done
  2. for x in 1 2 3; do echo $x; done

They both print:

1
2
3

My question is, why the two syntaxes? Is $x iterating through a different type of object in each of them?

Does bash make a similar distinction?

Addendum:

Why does the following work?:

#!/bin/zsh
a=1
b=2
c=5

d=(a b c)
for x in $d; do print $x;done

but this one doesn't?:

#!/bin/zsh
a=1
b=2
c=5

d=(a b c)

It complains with "parse error near `$d'"

for x $d; do print $x;done

  • 7
    for the example that "doesn't work", which is a csh style for loop, you're missing the parentheses. for x ($d); do print $x; done will work, and it will match the first syntax that you have enumerated at the beginning of your question. – Tim Kennedy Oct 25 '11 at 04:22
  • 1
    It is *SO crazy* - that those two statements - indeed do not both "work the same". I literally can't get my head around it! I need to smoke some of what those shell designers were smoking', back in the day, lol. – alex gray Apr 14 '14 at 03:34
  • 1
    Careful, there is more to this story than initially appears. I invite you to check my answer. – jasonleonhard Feb 07 '17 at 03:48
  • 1
    I didn't see anyone else mention it, but both forms allow omission of do/done: for i ({0..4..2}) for j ({a..c}) echo "($i,$j)" = {0,2,4}x{a,b,c}. Semicolons apply to the outermost loop and redirections apply to the innermost, and if you need to change that, you only need braces: for i ({0..4..2}) { for j ({a..c}) echo "($i,$j)" } | cat -n = {1,...,9}*({0,2,4}x{a,b,c}). Of course you can combine loops with zsh expansion: for i ("("{0..4..2}","{a..c}")") echo $i – John P Jun 08 '18 at 04:45

2 Answers2

109

Several forms of complex commands such as loops have alternate forms in zsh. These forms are mostly inspired by the C shell, which was fairly common when zsh was young but has now disappeared. These alternate forms act exactly like the normal forms, they're just a different syntax. They're slightly shorter, but less clear.

The standard form for the for command is for x in 1 2 3; do echo $x; done, and the standard form for the while command is while test …; do somecommand; done. Ksh, bash and zsh have an alternate form of for: for ((i = 0; i < 42; i++)); do somecommand; done, which mimics the for loops of languages like Pascal or C, to enumerate integers. Other exotic forms that exist in zsh are specific to zsh (but often inspired by csh).

  • Thanks a lot @Guilles. I am still a bit confused though. Which form is the specific one to zsh? Also, I have added one example at the end of my OP where I can't figure out how to translate between syntaxes. If the difference is just syntax, how should I fix the second script in the addendum to make it work? – Amelio Vazquez-Reina Oct 24 '11 at 23:20
  • 3
    @intrpc See my updated answer. As for your last example, it doesn't work because you wrote for x $d and zsh expects either in or ( where you wrote $d. Punctuation marks and reserved words can't come from a variable expansion, they have to be parsed before the shell can start on the variable expansions. – Gilles 'SO- stop being evil' Oct 24 '11 at 23:33
  • @intrpc both are zsh specific. zsh was specifically designed to support both bourne, korn, and c-shell syntax, as it's a hybrid of all three. – Tim Kennedy Oct 25 '11 at 04:25
  • For more brevity in a common case, zsh also lets you omit do ... done for a single command: for i in 1 2 3; echo $i. – Ulrich Schwarz Oct 25 '11 at 09:37
  • you can add for i in {1..3}; do echo $i ; done to the list of loops. (should work at least in bash and zsh) – pseyfert Jan 15 '16 at 21:22
  • 1
    @pseyfert It works, but it doesn't have any advantage over the forms I show: it's less portable than the for (( … )) form and uses a lot of memory if 3 is large. – Gilles 'SO- stop being evil' Jan 15 '16 at 23:35
  • @Gilles I was trying to grep through the output of a plugin using the command: for ((i = 1; i<100; i++)); ghi show $i | grep "log"; done. But it keeps giving me the error zsh: parse error neardone'. When I change the command tofor ((i = 1; i<100; i++)); ghi show $i | grep "log". It works fine. Any idea why? I typically try to stay inpythonbut am not very familiar withzsh` syntax... – alpha_989 Jun 09 '18 at 22:15
  • 2
    @alpha_989 In sh-like syntax, you need do after ));. Zsh also accepts another syntax for ((…)); COMMAND where COMMAND is a single command. That's why it accepts for ((i = 1; i<100; i++)); ghi show $i | grep "log". When it sees done after this, it complains because there was no do to end. See “Alternate Forms For Complex Commands” in the manual. – Gilles 'SO- stop being evil' Jun 09 '18 at 22:43
4

In my usecase I have many font files to patch (i.e. many .ttf files), and I want to directly type and run the script command in Terminal (not storing it in a script file):

for x in JetBrainsMonoNL* ; do \
    fontforge -script fontpatcher $x --mono -l -q --fontawesome \
        --octicons --fontlogos --mdi --powerline --powerlineextra; \
done;

The point is: JetBrainsMonoNL* will be the array to loop through, and if written in the first form:

for x in (JetBrainsMonoNL*)

the parentheses look redundant (at least to me, a newbie), and I think this is why the standard is defined without them.


I was leanring how to create this script and found this question (based on the title), and I think this answer will be helpful for newbie people like me. You can consider this as an real life example for the accepted answer.

Niing
  • 813