Shell syntax is prefix based. It has clauses introduced by special keywords. Certain clauses have to go together.
while loop is made out one or more testing commands:
test ; test ; test ; ...
and by one or more body commands:
body ; body ; body ; ...
Something has to tell the shell that a while loop begins. That's the purpose of the
while test ; test ; test ; ...
But then, things are ambiguous. Which command is the start of the body? Something has to indicate that, and that's what the
do prefix does:
do body ; body ; body ; ...
and, finally, something has to indicate that the last body has been seen; a special keyword
done does that.
These shell keywords don't require semicolon separation, even on the same line. For instance, if you close several nested loops, you can just have
done done done ....
Rather, the semicolon is between
... test ; body ... if they are on the same line. That semicolon is understood to be a terminator: it belongs with the
test. Therefore, if a
do keyword is inserted between them, it has to go between the semicolon and
body. If it were on the other side of the semicolon, it would be wrongly embedded inside the
test command's syntax, rather than placed between commands.
The shell syntax was originally designed by Stephen Bourne, and is inspired by Algol. Bourne loved Algol so much that he used lots of C macros in the shell source code to make C look like Algol. You can browse the 1979-dated shell sources from Version 7 Unix. The macros are in
mac.h, and they are used all over the place. For instance
if statements are rendered as