Bash variable scope and pipelines

I alluded to this nuance involving variable scope in my post on automating pdf processing, but I wanted to expand on it a bit.

Consider this little snippet:

i=0
printf "foo:bar:baz:quux" | grep -o '[^:]\+' | while read -r line ; do
   printf "Inner scope: %d - %s\n" $i $line
   ((i++))
   [ $i -eq 3 ] && break;
done
printf "====\nOuter scope\ni = %d\n" $i;

If you run this script - not in interactive mode in the shell - but as a script, what will i be in the outer scope? And why?

Unless you look carefully, you would think that i should be 3. After all, the while loop exits on a test for equality of i and 3, right? But no, i remains 0 in the outer scope; and this is because each command in a pipeline runs in its own subshell. From the GNU bash manual section on pipelines:

Each command in a pipeline is executed in its own subshell, which is a separate process. (Emphasis is mine.)

The last command in the pipeline in the above example is the while loop, so it’s merrily executing in its own subshell modifying its own i while the outer scope is unaware of what’s happening in this shell. This explains why i remains 0 in the outer scope.

But what are we to do if we want the inner scope to modify i? The key is to set the lastpipe option with shopt -s lastpipe. This option introduced in bash 4.2 forces the last command in the pipeline to run in the outer shell environment. So now if we modify the script with this option:

shopt -s lastpipe

i=0
printf "foo:bar:baz:quux" | grep -o '[^:]\+' | while read -r line ; do
   printf "Inner scope: %d - %s\n" $i $line
   ((i++))
   [ $i -eq 3 ] && break;
done
printf "====\nOuter scope\ni = %d\n" $i;

what is i in the outer scope? Right, it’s 3 this time because the while loop is executing in the shell environment, not in its own subshell.