Linux / Bash

Bash Variable Not Persisting Between Script Lines

When a variable seems to reset or lose its value partway through a Bash script, the cause is almost always that part of the script — commonly a loop fed by a pipe — is running in a subshell, which has its own separate copy of variables that disappears once that subshell finishes.

The Problem

You set a variable, use it inside a loop that appears to update it correctly, but after the loop finishes, the variable is back to its original value as if the loop never ran:

count=0
cat file.txt | while read line; do
    count=$((count + 1))
done
echo "Total: $count"
# prints "Total: 0", even though the loop clearly ran multiple times
Pipe creates a subshell Main shell count=0 (unchanged) | Subshell (while) count++ happens here only lost when subshell exits

Why It Happens

In Bash, piping data into a loop (or any command) causes that loop to run in a subshell — a separate child process with its own copy of the shell's variables. Any changes made to variables inside that subshell are completely invisible to the parent shell once the subshell exits, even though the syntax looks like ordinary sequential code. The most common causes are:

  • Piping into a while read loop, as shown above, which is by far the most common version of this issue
  • Running a command substitution or grouped command block that Bash decides to execute in a subshell for other reasons
  • Explicitly backgrounding part of a script with &, which also runs in a separate process with its own variable scope

The Fix

The most common fix is restructuring the loop to avoid the pipe entirely, using process substitution instead, which keeps the loop running in the main shell:

count=0
while read line; do
    count=$((count + 1))
done < file.txt
echo "Total: $count"
# now correctly prints the real count

Notice the difference: instead of piping the file's content into the loop, the file is redirected as input to the while loop directly using <. This avoids creating a subshell, since there's no pipe involved — the loop runs in the same shell as everything around it.

If you're piping the output of a command (not just a plain file) into the loop, process substitution handles that case too:

count=0
while read line; do
    count=$((count + 1))
done < <(grep "ERROR" logfile.txt)
echo "Total errors: $count"

This syntax, < <(command), feeds the command's output as if it were a file, again avoiding the pipe-induced subshell.

Still Not Working?

If restructuring away from a pipe isn't practical for your specific case, an alternative is writing the value to a temporary file from inside the subshell, then reading it back in the parent shell after the loop finishes:

cat file.txt | while read line; do
    count=$((count + 1))
    echo "$count" > /tmp/count.txt
done
count=$(cat /tmp/count.txt)
echo "Total: $count"

This works around the subshell limitation using the filesystem as a bridge, which is less elegant than avoiding the subshell entirely, but useful when the loop's source genuinely has to remain a pipe for other reasons.

Another option, available in Bash 4.2 and later, is enabling the lastpipe shell option, which makes the last command in a pipeline run in the current shell rather than a subshell, specifically when job control is disabled (which it typically is inside a non-interactive script):

shopt -s lastpipe
count=0
cat file.txt | while read line; do
    count=$((count + 1))
done
echo "Total: $count"

This preserves the original pipe-based syntax while avoiding the subshell problem, but it only works reliably inside scripts (not interactive terminal sessions) and depends on the specific Bash version installed, so testing it directly in your target environment before relying on it is worthwhile.

Whichever approach you choose, it helps to remember the general rule going forward: any time a variable needs to persist outside of a pipeline, restructure the pipeline so the variable-modifying logic runs in the main shell, using input redirection or process substitution rather than a pipe, whenever that's a practical option for the task at hand.