This is a common error while using pipe over while loops. Consider this shell snippet:
#!/bin/sh cat file.txt | while read line do echo "inside loop" exit 1 done echo "outside loop" exit 0
You’d expect the script to exit on the first line in file.txt. However execute this script and you have:
inside loop outside loop
It is as if the exit 1
inside the loop is ignored. Another example:
#!/bin/sh a=0 cat file.txt | while read line do echo "inside loop" a=1 done echo "outside loop" echo "a=$a"
Here you’d expect the value of a to be 1 at the end of the script. Instead, if you execute this you have:
inside loop outside loop a=0
It’s as if the variable a isn’t even updated. In fact it is, though only inside the loop. So what is happening here?
The pipe (|
) you use to feed the loop creates a subshell. In fact this is really just another process. So the exit 1
or a=1
only apply to these piped processes.
How can you fix that?
In the simple case presented above, you can simply use file redirection:
while read line do ... done < file.txt
But what if you really want to feed the loop with the output of another process. Like you would do with find for instance.
If you use bash you can use process substitution as described here. But you shouldn’t use bash for scripting anyway. For shell scripting you might be tempted to use a temporary file to store the process output:
# Use a temporary file. tmp=$(mktemp) find . > $tmp while read line do ... done < $tmp rm $tmp
However this consumes disk space, and the loop only starts after the find process exited. Another option would be to use a named fifo:
fifo=$(mktemp -u) mkfifo $fifo find . > $fifo & while read file do ... done < $fifo rm $fifo
This time you create a single file, yet no disk space is used (apart for the fifo inode itself). Also the find command is a child process, so the loop reads find output as it comes.
Although the version above already works as it should, you may want to use an anonymous fifo. This way you only need to create a fifo file, although you can delete it immediatly. You can achieve this with a little help from our beloved file descriptor 3.
fifo=$(mktemp -u) # Create fifo mkfifo $fifo # Create fd 3 and unlink fifo file. exec 3<> $fifo rm $fifo # Redirect find to fd 3. find . >&3 & # Feed fd 3 to while loop. while read line do ... done <&3 # Close fd 3. exec 3>&-