67

I have a problem in one of my shell scripts. Asked a few colleagues, but they all just shake their heads (after some scratching), so I've come here for an answer.

According to my understanding the following shell script should print "Count is 5" as the last line. Except it doesn't. It prints "Count is 0". If the "while read" is replaced with any other kind of loop, it works just fine. Here's the script:

echo "1">input.data
echo "2">>input.data
echo "3">>input.data
echo "4">>input.data
echo "5">>input.data

CNT=0 

cat input.data | while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done 
echo "Count is $CNT"

Why does this happen and how can I prevent it? I've tried this in Debian Lenny and Squeeze, same result (i.e. bash 3.2.39 and bash 4.1.5. I fully admit to not being a shell script wizard, so any pointers would be appreciated.

wolfgangsz
  • 9,007

7 Answers7

50

This is kind of a 'common' mistake. Pipes create SubShells, so the while read is running on a different shell than your script, that makes your CNT variable never changes (only the one inside the pipe subshell).

Group the last echo with the subshell while to fix it (there are many other way to fix it, this is one. Iain and Ignacio's answers have others.)

CNT=0

 cat input.data | ( while read 
do
  let CNT++;
  echo "Counting to $CNT"
done 
echo "Count is $CNT" )

Long explanation:

  1. You declare CNT on your script to be value 0;
  2. A SubShell is started on the | to while read;
  3. Your $CNT variable is exported to the SubShell with value 0;
  4. The SubShell counts and increase the CNT value to 5;
  5. SubShell ends, variables and values are destroyed (they don't get back to the calling process/script).
  6. You echo your original CNT value of 0.
coredump
  • 12,921
35

See argument @ Bash FAQ entry #24: "I set variables in a loop. Why do they suddenly disappear after the loop terminates? Or, why can't I pipe data to read?" (most recently archived here).

Summary: This is only supported from bash 4.2 and up. You need to use different ways like command substitutions instead of a pipe if you are using bash.

15

This works

CNT=0 

while read ;
do
  let CNT++;
  echo "Counting to $CNT"
done <input.data
echo "Count is $CNT"
user9517
  • 117,122
10

Try passing the data in a sub-shell instead, like it's a file before the while loop. This is similar to lain's solution, but assumes you don't want some intermittent file:

total=0
while read var
do
  echo "variable: $var"
  ((total+=var))
done < <(echo 45) #output from a command, script, or function
echo "total: $total"
Steve
  • 101
  • 1
  • 3
5

Another solution is just simply add shopt -s lastpipe before the while loop.

I say, if the trouble comes cause the while is in the last segment of the pipeline, and in Bash all the commands in a pipeline executes in a subshell in a separated process isolated one from the other, then, using the lastpipe will execute the last command in the pipeline in the foreground.

For example:

CNT=0
shopt -s lastpipe
cat input.data | while read ;
...

And almost everything stay the same.

Cuauhtli
  • 151
0

I found a way using the stderr file to store the value of var i.

# reading lines of content from 2 files concatenated
# inside loop: write value of var i to stderr (before iteration)
# outside: read var i from stderr, has last iterative value
f=/tmp/file1
g=/tmp/file2

i=1 cat $f $g |
while read -r s; do echo $s > /dev/null; # some work echo $i > 2 let i++ done; read -r i < 2 echo $i

bugdot
  • 1
0

What worked for me in Bash 4.2.x was using lastpipe, by adding these two lines to the beginning of my bash script, right after the shebang line (#!/bin/bash):

set +m
shopt -s lastpipe

Acording to http://mywiki.wooledge.org/BashFAQ/024:

+m: Disables monitor mode (job control) in an interactive shell since it is on by default there and it needs to be disabled for lastpipe to work. (...) Bash 4.2 introduces the aforementioned ksh-like behavior to Bash. The one caveat is that job control must not be enabled, thereby limiting its usefulness in an interactive shell.

Luis Talora
  • 111
  • 2