When passing program output into a compound command with a pipe, there's a subtle pattern of problems, which can be very hard to figure out, to do with subshells.
Consider this simple loop, which is intended to count the lines of output from a call to who, and then print it:
((count = 0))
who | while read -r ; do
((count++))
done
printf '%u\n' "$count"
When run, however, this program always prints zero for the count! Most new shell programmers eventually run into this sort of particularly baffling problem.
Because each command after the first command in a pipeline runs in its own subshell environment—its own forked process—changes to variables do not persist after the pipeline is completed. This means that the count variable inside the while loop correctly counts the input lines, but that count is discarded when the subshell ends, and the value is back at zero!
When the data is saved to a temporary file first, however, and the while loop is passed that file as input, the while loop runs in the same process, not in a subshell, and so the line-counting works correctly:
who > who.out
while read -r ; do
((count++))
done < who.out
printf '%u\n' "$count"
There are a few ways to deal with this without having to involve (and clean up) a temporary file. First, we can use a compound command to include all of the code that needs to use the count variable as part of the same subshell:
who | {
while read -r ; do
((count++))
done
printf '%u\n' "$count"
}
This method works even in non-Bash shells, but it can be unwieldy if the code inside the pipeline is extensive. An easier method specific to Bash is with process substitution:
while read -r ; do
((count++))
done < <(who)
printf '%u\n' "$count"
Note the < <(who) syntax at the very end of the loop. The first left angle bracket, <, is a standard input redirection, with which we're already familiar. The new syntax in <(who) runs the command inside the parentheses and expands to a temporary file handle or filename, managed internally by Bash, containing the command's output.
As an alternative, there is also the lastpipe option set with shopt, which makes the last command in a pipeline run in the current shell, not a subshell:
shopt -s lastpipe
((count = 0))
who | while read -r ; do
((count++))
done
printf '%u\n' "$count"
This works in a script, but we recommend you use process substitution instead, as it behaves reliably whether the script is executed or sourced into an interactive shell.