Nicely spotted! I don't have a definite answer, but here is what the source code says about it:
It's indeed not valid in the original Bourne shell from AT&T UNIX v7:
(shell has just read `for name`):
IF skipnl()==INSYM
THEN chkword();
t->forlst=item(0);
IF wdval!=NL ANDF wdval!=';'
THEN synbad();
FI
chkpr(wdval); skipnl();
FI
chksym(DOSYM|BRSYM);
Given this snippet, it does not appear to be a conscious design decision. It's just a side effect of the semicolon being handled as part of the in
group, which is skipped entirely when there is no "in".
Dash agrees that it's not valid in Bourne, but adds it as an extension:
/*
* Newline or semicolon here is optional (but note
* that the original Bourne shell only allowed NL).
*/
Ksh93 claims that it's valid, but says nothing of the context:
/* 'for i;do cmd' is valid syntax */
else if(tok==';')
while((tok=sh_lex(lexp))==NL);
Bash has no comment, but explicitly adds support for this case:
for_command: FOR WORD newline_list DO compound_list DONE
{
$$ = make_for_command ($2, add_string_to_list (""$@"", (WORD_LIST *)NULL), $5, word_lineno[word_top]);
if (word_top > 0) word_top--;
}
...
| FOR WORD ';' newline_list DO compound_list DONE
{
$$ = make_for_command ($2, add_string_to_list (""$@"", (WORD_LIST *)NULL), $6, word_lineno[word_top]);
if (word_top > 0) word_top--;
}
In zsh, it is's just a side effect of the parser:
while (tok == SEPER)
zshlex();
where (SEPER
is ;
or linefeed). Due to this, zsh happily accepts this loop:
for foo; ;
;
; ; ; ; ;
; do echo cow; done
To me, this all points to an intentional omission in POSIX, and widely and intentionally supported as an extension.