115

Bash allows to use: cat <(echo "$FILECONTENT")

Bash also allow to use: while read i; do echo $i; done </etc/passwd

to combine previous two this can be used: echo $FILECONTENT | while read i; do echo $i; done

The problem with last one is that it creates sub-shell and after the while loop ends variable i cannot be accessed any more.

My question is:

How to achieve something like this: while read i; do echo $i; done <(echo "$FILECONTENT") or in other words: How can I be sure that i survives while loop?

Please note that I am aware of enclosing while statement into {} but this does not solves the problem (imagine that you want use the while loop in function and return i variable)

codeforester
  • 39,467
  • 16
  • 112
  • 140
Wakan Tanka
  • 7,542
  • 16
  • 69
  • 122
  • http://mywiki.wooledge.org/BashFAQ/024 – tripleee Sep 22 '14 at 03:27
  • Related: https://stackoverflow.com/questions/37229058/append-to-an-array-variable-from-a-pipeline-command . Explains all options including the below-mentioned process substitution and `lastpipe` and their pros and cons. – ivan_pozdeev Jun 21 '17 at 23:02
  • 1
    Related: [A variable modified inside a while loop is not remembered](https://stackoverflow.com/questions/16854280/a-variable-modified-inside-a-while-loop-is-not-remembered). – codeforester Aug 01 '17 at 21:10
  • @codeforester there is more the one dup... Related: [Why piping input to "read" only works when fed into "while read ..." construct?](https://stackoverflow.com/a/13764018/1765658) and [Read values into a shell variable from a pipe](https://stackoverflow.com/q/2746553/1765658) – F. Hauri - Give Up GitHub Jun 13 '22 at 13:21

5 Answers5

158

The correct notation for Process Substitution is:

while read i; do echo $i; done < <(echo "$FILECONTENT")

The last value of i assigned in the loop is then available when the loop terminates. An alternative is:

echo $FILECONTENT | 
{
while read i; do echo $i; done
...do other things using $i here...
}

The braces are an I/O grouping operation and do not themselves create a subshell. In this context, they are part of a pipeline and are therefore run as a subshell, but it is because of the |, not the { ... }. You mention this in the question. AFAIK, you can do a return from within these inside a function.


Bash also provides the shopt builtin and one of its many options is:

lastpipe

If set, and job control is not active, the shell runs the last command of a pipeline not executed in the background in the current shell environment.

Thus, using something like this in a script makes the modfied sum available after the loop:

FILECONTENT="12 Name
13 Number
14 Information"
shopt -s lastpipe   # Comment this out to see the alternative behaviour
sum=0
echo "$FILECONTENT" |
while read number name; do ((sum+=$number)); done
echo $sum

Doing this at the command line usually runs foul of 'job control is not active' (that is, at the command line, job control is active). Testing this without using a script failed.

Also, as noted by Gareth Rees in his answer, you can sometimes use a here string:

while read i; do echo $i; done <<< "$FILECONTENT"

This doesn't require shopt; you may be able to save a process using it.

Community
  • 1
  • 1
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • 2
    Pardon my ignorance. I know this is right solution and I've marked it as answer so it worked for me. But now when I run `while read i; do echo $i; done < <(cat /etc/passwd); echo $i` It did not return last line two times. What I am doing wrong? – Wakan Tanka Feb 22 '15 at 19:48
  • @WakanTanka: I had to experiment a bit...I believe the answer is that the failing read resets `i` to empty, so the echo after the loop echoes a blank line. – Jonathan Leffler Feb 22 '15 at 21:54
  • 1
    Kudos for the Process Substitution reference. I was unaware of. – Atcold Sep 19 '16 at 20:00
  • @Wakan Tanka: got same result as yours, i use `while read i; do x=$i; done < <(cat /etc/passwd); echo i=$i; echo x=$x` worked, //maybe those days bash behavior changed ? – yurenchen Nov 18 '18 at 19:32
  • You're a life saver! Been looking for hours for a similar solution. Yours is the only one that worked. – Pat Nov 06 '19 at 11:21
33

Jonathan Leffler explains how to do what you want using process substitution, but another possibility is to use a here string:

while read i; do echo "$i"; done <<<"$FILECONTENT"

This saves a process.

Community
  • 1
  • 1
Gareth Rees
  • 64,967
  • 9
  • 133
  • 163
1

This function makes duplicates $NUM times of jpg files (bash)

function makeDups() {
NUM=$1
echo "Making $1 duplicates for $(ls -1 *.jpg|wc -l) files"
ls -1 *.jpg|sort|while read f
do
  COUNT=0
  while [ "$COUNT" -le "$NUM" ]
  do
    cp $f ${f//sm/${COUNT}sm}
    ((COUNT++))
  done
done
}
Steve
  • 21
  • 2
0

The fastest was:

while read -r ITEM; do
    OPERATION
done < <(COMMAND)

Or in one line:

while read -r ITEM; do OPERATION; done < <(COMMAND)
0

Bash provides var default behavior so:

while read -r line
do
  total=${total:-0}
  total=$((total + $(calculation code)))
done
echo $total

This allows the loop to easily capture stdin, for scripts and functions.

ocodo
  • 29,401
  • 18
  • 105
  • 117