This simpler example might help:
$ c=0
$ printf 'a\nb\nc\n' | while read i; do (( c++ )); echo "c is now $c"; done
c is now 1
c is now 2
c is now 3
$ echo "$c"
0
As you can see, this reproduces the behavior you observe in your script. The reason is that because you're piping data to the while, that means it will launch a subshell which inherits a copy of all the parent's variables, but then, when the loop exits, it doesn't export those copies back to the parent. In other words, you didn't increment your COUNTER variable, you incremented a copy of the variable that was destroyed as soon as your loop ended.
You can see this in action if you try this modified version of your script:
#!/bin/bash
COUNTER=0
function increment {
echo "increment called"
((COUNTER++))
}
function report {
echo "COUNTER: $COUNTER ($1)"
}
function reset_counter {
COUNTER=0
}
function increment_if_yes {
answer=$1
if [ "$answer" == "yes" ]
then
increment
fi
}
function break_it {
echo "aa COUNTER at start of break_it: $COUNTER"
echo -e "maybe\nyes\nno" | \
while read LL
do
echo "bb COUNTER in loop top: $COUNTER"
increment_if_yes $LL
echo "bb COUNTER in loop bottom: $COUNTER"
done
echo "aa COUNTER at end of break_it: $COUNTER"
}
report start # counter should be 0
increment
report one
increment_if_yes yes
report two
increment_if_yes no
report "still two"
reset_counter
report reset
break_it
report "I'd expect one"
Running this prints:
COUNTER: 0 (start)
increment called
COUNTER: 1 (one)
increment called
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
aa COUNTER at start of break_it: 0
bb COUNTER in loop top: 0
bb COUNTER in loop bottom: 0
bb COUNTER in loop top: 0
increment called
bb COUNTER in loop bottom: 1
bb COUNTER in loop top: 1
bb COUNTER in loop bottom: 1
aa COUNTER at end of break_it: 0
COUNTER: 0 (I'd expect one)
Note how the bb COUNTER values show that a variable named $COUNTER was incremented in your break_it function, it's just that this is actually the copy of the variable and not the one available in the rest of the script.
Finally, you might want to read through the Command Execution Environment section of the bash manual, especially (emphasis mine):
When a simple command other than a builtin or shell function is to be
executed, it is invoked in a separate execution environment that
consists of the following. Unless otherwise noted, the values are
inherited from the shell.
- the shell’s open files, plus any modifications and additions specified by redirections to the command
- the current working directory
- the file creation mode mask
- shell variables and functions marked for export, along with variables exported for the command, passed in the environment (see
Environment)
- traps caught by the shell are reset to the values inherited from the shell’s parent, and traps ignored by the shell are ignored
A command invoked in this separate environment cannot affect the
shell’s execution environment.
That last sentence is the crux of your issue: commands invoked in separate environments cannot affect the parent environment and this is why you cannot increment your variable the way you want.
You can, however, since your shell (bash) supports process substitution, change your function to this and it will work:
function break_it {
while read LL
do
increment_if_yes $LL
done < <(printf 'maybe\nyes\nno\n')
}
If I now run your original script, but with the break_it function modified as shown above, I get:
$ foo.sh
COUNTER: 0 (start)
COUNTER: 1 (one)
COUNTER: 2 (two)
COUNTER: 2 (still two)
COUNTER: 0 (reset)
COUNTER: 1 (I'd expect one)