home of the madduck/ blog/
Shell sucks

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!