In the following program, if I set the variable $foo to the value 1 inside the first if statement, it works in the sense that its value is remembered after the if statement. However, when I set the same variable to the value 2 inside an if which is inside a while statement, it's forgotten after the while loop. It's behaving like I'm using some sort of copy of the variable $foo inside the while loop and I am modifying only that particular copy. Here's a complete test program:

#!/bin/bashset -eset -u foo=0bar="hello" if [[ "$bar"=="hello" ]]thenfoo=1echo "Setting \$foo to 1: $foo"fiecho "Variable \$foo after if statement: $foo" lines="first line\nsecond line\nthird line" echo -e $lines | while read linedoif [[ "$line"=="second line" ]]thenfoo=2echo "Variable \$foo updated to $foo inside if inside while loop"fiecho "Value of \$foo in while loop body: $foo"doneecho "Variable \$foo after while loop: $foo"# Output:# $ http://stackoverflow.com/testbash.sh# Setting $foo to 1: 1# Variable $foo after if statement: 1# Value of $foo in while loop body: 1# Variable $foo updated to 2 inside if inside while loop# Value of $foo in while loop body: 2# Value of $foo in while loop body: 2# Variable $foo after while loop: 1# bash --version# GNU bash, version 4.1.10(4)-release (i686-pc-cygwin)
share|improve this question
up vote136down voteaccepted
echo -e $lines | while read line ...done

The while loop is executed in a subshell. So any changes you do to the variable will not be available once the subshell exits.

Instead you can use a here string to re-write the while loop to be in the main shell process; only echo -e $lines will run in a subshell:

while read linedoif [[ "$line"=="second line" ]]thenfoo=2echo "Variable \$foo updated to $foo inside if inside while loop"fiecho "Value of \$foo in while loop body: $foo"done <<< "$(echo -e "$lines")"
share|improve this answer
  • 7
    better change <<< "$(echo -e "$lines")" to simple <<< "$lines"– beliyMay 18 '17 at 14:43

UPDATED#2

Explanation is in Blue Moons's answer.

Alternative solutions:

Eliminate echo

while read line; do...done <<EOTfirst linesecond linethird lineEOT

Add the echo inside the here-is-the-document

while read line; do...done <<EOT$(echo -e $lines)EOT

Run echo in background:

coproc echo -e $lineswhile read -u ${COPROC[0]} line; do ...done

Redirect to a file handle explicitly (Mind the space in < <!):

exec 3< <(echo -e $lines)while read -u 3 line; do...done

Or just redirect to the stdin:

while read line; do...done < <(echo -e $lines)

And one for chepner (eliminating echo):

arr=("first line" "second line" "third line");for((i=0;i<${#arr[*]};++i)) { line=${arr[i]}; ...}

Variable $lines can be converted to an array without starting a new sub-shell. The characters \ and n has to be converted to some character (e.g. a real new line character) and use the IFS (Internal Field Separator) variable to split the string into array elements. This can be done like:

lines="first line\nsecond line\nthird line"echo "$lines"OIFS="$IFS"IFS=$'\n' arr=(${lines//\\n/$'\n'}) # ConversionIFS="$OIFS"echo "${arr[@]}", Length: ${#arr[*]}set|grep ^arr

Result is

first line\nsecond line\nthird linefirst line second line third line, Length: 3arr=([0]="first line" [1]="second line" [2]="third line")
share|improve this answer
  • +1 for the here-doc, since the lines variable's only purpose seems to be to feed the while loop.– chepnerMay 31 '13 at 12:44
  • @chepner: Thx! I added another one, dedicated to You!– TrueYMay 31 '13 at 12:57
  • There is yet another solution given here: for line in $(echo -e $lines); do ... done– dma_kJan 19 '16 at 20:20
  • @dma_k Thanks for your comment! This solution would result 6 lines containing a single word. OP's request was different...– TrueYJan 20 '16 at 8:59
  • upvoted. running echo in a subshell inside here-is, was one of the few solutions that worked in ash– DavidNov 26 '16 at 5:03

You are the 742342nd user to ask this bash FAQ. The answer also describes the general case of variables set in subshells created by pipes:

E4) If I pipe the output of a command into read variable, whydoesn't the output show up in $variable when the read command finishes?

This has to do with the parent-child relationship between Unixprocesses. It affects all commands run in pipelines, not justsimple calls to read. For example, piping a command's outputinto a while loop that repeatedly calls read will result inthe same behavior.

Each element of a pipeline, even a builtin or shell function,runs in a separate process, a child of the shell running thepipeline. A subprocess cannot affect its parent's environment. When the read command sets the variable to the input, thatvariable is set only in the subshell, not the parent shell. Whenthe subshell exits, the value of the variable is lost.

Many pipelines that end with read variable can be convertedinto command substitutions, which will capture the output ofa specified command. The output can then be assigned to avariable:

grep ^gnu /usr/lib/news/active | wc -l | read ngroup

can be converted into

ngroup=$(grep ^gnu /usr/lib/news/active | wc -l)

This does not, unfortunately, work to split the text amongmultiple variables, as read does when given multiple variablearguments. If you need to do this, you can either use thecommand substitution above to read the output into a variableand chop up the variable using the bash pattern removalexpansion operators or use some variant of the followingapproach.

Say /usr/local/bin/ipaddr is the following shell script:

#! /bin/shhost `hostname` | awk '/address/ {print $NF}'

Instead of using

/usr/local/bin/ipaddr | read A B C D

to break the local machine's IP address into separate octets, use

OIFS="$IFS"IFS=.set -- $(/usr/local/bin/ipaddr)IFS="$OIFS"A="$1" B="$2" C="$3" D="$4"

Beware, however, that this will change the shell's positionalparameters. If you need them, you should save them before doingthis.

This is the general approach -- in most cases you will not need toset $IFS to a different value.

Some other user-supplied alternatives include:

read A B C D << HERE$(IFS=.; echo $(/usr/local/bin/ipaddr))HERE

and, where process substitution is available,

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))
share|improve this answer
  • 4
    You forgot the Family Feud problem. Sometimes it’s very hard to find the same combination of words as whoever wrote the answer, so that you’re neither flooded in wrong results, nor filter out said answer.– Evi1M4chineJan 29 '16 at 19:20

Hmmm... I would almost swear that this worked for the original Bourne shell, but don't have access to a running copy just now to check.

There is, however, a very trivial workaround to the problem.

Change the first line of the script from:

#!/bin/bash

to

#!/bin/ksh

Et voila! A read at the end of a pipeline works just fine, assuming you have the Korn shell installed.

share|improve this answer

    How about a very simple method

     +call your while loop in a function - set your value inside (nonsense, but shows the example)- return your value inside +capture your value outside+set outside+display outside#!/bin/bash# set -e# set -u# No idea why you need this, not using herefoo=0bar="hello"if [[ "$bar"=="hello" ]]thenfoo=1echo "Setting \$foo to $foo"fiecho "Variable \$foo after if statement: $foo"lines="first line\nsecond line\nthird line"function my_while_loop{echo -e $lines | while read linedoif [[ "$line"=="second line" ]]thenfoo=2; return 2;echo "Variable \$foo updated to $foo inside if inside while loop"fiecho -e $lines | while read linedoif [[ "$line"=="second line" ]]thenfoo=2; echo "Variable \$foo updated to $foo inside if inside while loop"return 2;fi# Code below won't be executed since we returned from function in 'if' statement# We aready reported the $foo var beint set to 2 anywayecho "Value of \$foo in while loop body: $foo"done}my_while_loop; foo="$?"echo "Variable \$foo after while loop: $foo"Output:Setting $foo 1Variable $foo after if statement: 1Value of $foo in while loop body: 1Variable $foo after while loop: 2bash --versionGNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)Copyright (C) 2007 Free Software Foundation, Inc.
    share|improve this answer
    • 6
      Maybe there's a decent answer under the surface here, but you've butchered the formatting to the point that it's unpleasant to try and read.– Mark AmerySep 10 '16 at 19:42
    • You mean the original code is pleasant to read? (I've just followed :p)– MarcinAug 11 '17 at 14:20

    Your Answer

     
    discard

    By posting your answer, you agree to the privacy policy and terms of service.

    Not the answer you're looking for? Browse other questions tagged or ask your own question.