Loops, command grouping and a complete script
◈ 11 cardsChoose between for, while and until; iterate the arguments safely; interrupt a loop with break and continue; know which grouping construct forks; and assemble the pieces into a whole working script.
while and until are one loop, inverted
Here is a guessing loop, written twice. The first version:
secret=7
guess=
while [ "$guess" != "$secret" ]
do
printf "guess: "
read guess
done
echo "correct"
And the second:
secret=7
guess=
until [ "$guess" = "$secret" ]
do
printf "guess: "
read guess
done
echo "correct"
Everything is identical except the keyword and one operator. while repeats the body while the condition command succeeds — while it exits zero. until repeats the body while the condition command fails. That is the entire difference: neither is more powerful, neither is faster, and anything one can express the other can express with the condition negated.
So the choice is a readability choice, and it is worth making deliberately. Reach for the form that lets you write the condition without a !, because a condition you have to mentally invert while reading is a condition you will eventually get wrong.
for walks a list that is fixed before the first iteration
for f in *.txt
do
echo "$f"
done
The list after in is expanded once, before the loop starts. Adding a file inside the body does not lengthen the loop.
for with no in list at all iterates the positional parameters:
for f
do
echo "$f"
done
This is not a special case to memorise separately — it is defined to be exactly for f in "$@", the quoted form. Which means the bare version is safe by construction, and for f in $* is the version that quietly breaks on an argument containing a space.
break leaves the innermost loop immediately. continue abandons the rest of the body and starts the next iteration. Both take an optional count, so break 2 escapes two levels of nesting at once.
case, and the semicolons that are not optional
case "$reply" in
y|Y|yes) echo "proceeding" ;;
n|N|no) echo "stopping" ;;
*) echo "please answer y or n" ;;
esac
A vertical bar separates alternative patterns for one branch. The patterns are shell patterns, so *) is the catch-all — and leaving it out means bad input produces silence, which every user reads as success.
Now the semicolons, because the textbook describes them incorrectly and the exam does not. A missing ;; is not a fall-through into the next branch. On a POSIX shell it is a syntax error: the shell parses the whole case before running any of it, complains at the next ) it cannot account for, and nothing in the script executes at all — not even the lines above the case. That is a better failure than silent fall-through, because you find out immediately, but it means "the script does nothing and prints a syntax error" is a symptom of a missing ;;. The only one you may omit is the last, immediately before esac. Some shells add ;& as a deliberate fall-through operator; it is an extension and is not part of the Bourne shell this course assumes.
Two kinds of brace, and only one of them forks
( command-list ) runs the list in a subshell — the shell forks a child and the child does the work. { command-list; } runs the list in the current shell. The space after the opening brace and the semicolon (or newline) before the closing one are both required, because { and } are reserved words rather than punctuation, and a word needs whitespace around it.
Watch what actually differs:
$ count=0
$ ( count=5; echo "inside: $count" )
inside: 5
$ echo "outside: $count"
outside: 0
$ { count=9; }
$ echo "after braces: $count"
after braces: 9
The subshell's assignment died with the subshell. The brace group's assignment is still there afterwards. The same asymmetry applies to the working directory: ( cd /tmp; ls ) leaves you exactly where you started, which makes the parenthesised form a tidy way to visit a directory without having to remember to come back. { cd /tmp; ls; } really does move you.
One correction worth writing into your notes. It is tempting to prove which form forked by printing the shell process ID inside each, and it does not work: POSIX defines that variable as the process ID of the invoking shell, and it keeps that same value inside a subshell. The observable difference between the two constructs is scope — variables and working directory — not the process ID they report.
Redirection applies to the whole group either way, with a single open of the file: { echo one; echo two; } > pair.txt writes both lines.
Putting it together
Every piece of this module now fits into one shape that the assignments keep asking for: take an argument or read one, validate it, test the file system, branch, act, and exit with a status that says what happened. The written exam below is that shape at full size.