Shell programming must be the most depressive waste of time out there, and if only because things sometimes just don’t work the way you’d expect from a scripting language.
Take the following simple examples in bash (3):
for i in 1 2 3; do
echo $i
exit 1
done
echo END
==> 1
echo -e "1\n2\n3" | while read i; do
echo $i
exit 1
done
echo END
==> 1
==> END
WTF? The result of the first — the whole script exits entirely
after the first iteration — is what I expect. After all, I did not
call break, but exit. The result of the
second is totally unexpected. I understand that for
and while each execute their iterations in a subshell,
but why don’t these behave in the same way?
The fix here is a || exit $? following the
done keyword. Urks!
Here’s another one: the task is simply to output the number of
1’s in the output (yeah, I realise grep -c can do this
too):
cnt=0
echo -e "2\n1\n3\n1" | while read i; do
case $i in
1) cnt=$((cnt + 1));;
esac
done
echo $cnt
==> 0
The reason? The while subshell receives a
copy of the caller environment, so when it updates
$cnt, it only updates a copy of the actual variable,
leaving the original untouched. Similarly, a variable defined
inside the subshell won’t be available after the while
loop is done.
I cannot find a fix that’s not bash specific.
Of course, it works fine when I replace the while
loop by a for loop:
cnt=0
for i in 2 1 3 1; do
case $i in
1) cnt=$((cnt + 1));;
esac
done
echo $cnt
==> 2
Great, isn’t it?
And I am not surprised that zsh gets it right. Why
would anyone actually want to use bash?
Update: here’s Joey’s explanation for the behaviour, and Clint’s account for why zsh does it right. I still can’t figure out how to count the 1’s.
Update \^2: Axel Liljencrantz sent in this message.
Update \^3: Alexander Sieck showed me that process substitution (“\<(command)”) instead of the pipe does work (for both cases):
while read i; do
echo $i
exit 1
done < <(echo -e "1\n2\n3")
echo END
Of course that’s nowhere near POSIX compliance…
Update \^4: my solution, which allows for spaces in the lines:
IFSOLD=${IFS:-}
IFS='
' # yes, a newline
for i in $(the_command): do
IFS=$IFSOLD
...
done
What a hack!

