Bash: Scope of variables in a for loop using tee
Once you put for loop into a pipe, it runs in a subshell which does not pass its variables to supershell.
Each side of a pipe is executed in a subshell. A subshell is copy of the original shell process which starts in the same state and then evolves independently¹. Variables set in the subshell cannot escape back to the parent shell.
In bash, instead of using a pipe, you could use process substitution. This would behave almost identically to the pipe, except that the loop executes in the original shell and only tee
executes in a subshell.
for num in "${numbers[@]}"; do
if ((num > max)); then
echo "old -> new max = $max -> $num"
max=$num
fi
done > >(tee logfile)
echo "Max= $max"
While I was at it I changed a few things in your script:
- Always use double quotes around variable substitutions unless you know why you need to leave them out.
- Don't use
&&
when you meanif
. It's less readable and doesn't have the same behavior if the command's return status is used. - Since I'm using bash-specific constructs, I might as well use arithmetic syntax.
Note that there is a small difference between the pipe solution and the subshell solution: a pipe command finishes when both commands exit, whereas a command with a process substitution finishes when the main command exits, without waiting for the process in the process substitution. This means that when your script finishes, the log file may not be fully written.
Another approach is to use some other channel to communicate from the subshell to the original shell process. You can use a temporary file (flexible, but harder to do right — write the temporary file to an appropriate directory, avoid name conflicts (use mktemp
), remove it even in case of errors). Or you can use another pipe to communicate the result and grab the result in a command substitution.
max=$({ { for … done;
echo "$max" >&4
} | tee logfile >&3; } 4>&1) 3&>1
The output of tee
goes to file descriptor 3 which is redirected to the script's standard output. The output of echo "$max"
goes to file descriptor 4 which is redirected into the command substitution.
It doesn't survive the pipe, but this works:
numbers="1 111 5 23 56 211 63"
max=0
echo $max>maxfile
for num in ${numbers[@]}; do
[ $num -gt $max ]\
&& echo "old -> new max = $max -> $num"\
&& max=$num\
&& echo $max>maxfile
done | tee logfile
read max<maxfile
echo "Max= $max"