whileループ内で変更された変数は記憶されません


187

次のプログラムで$fooは、最初のifステートメント内で変数に値1 を設定すると、ifステートメントの後にその値が記憶されるという意味で機能します。ただし、同じ変数をステートメントif内の内の値2に設定するとwhilewhileループの後で忘れられます。ループ$foo内で変数のある種のコピーを使用していて、whileその特定のコピーのみを変更しているように動作しています。ここに完全なテストプログラムがあります:

#!/bin/bash

set -e
set -u 
foo=0
bar="hello"  
if [[ "$bar" == "hello" ]]
then
    foo=1
    echo "Setting \$foo to 1: $foo"
fi

echo "Variable \$foo after if statement: $foo"   
lines="first line\nsecond line\nthird line" 
echo -e $lines | while read line
do
    if [[ "$line" == "second line" ]]
    then
    foo=2
    echo "Variable \$foo updated to $foo inside if inside while loop"
    fi
    echo "Value of \$foo in while loop body: $foo"
done

echo "Variable \$foo after while loop: $foo"

# Output:
# $ ./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)


shellcheckユーティリティはこれをキャッチします(github.com/koalaman/shellcheck/wiki/SC2030を参照)。カットとで上記のコードを貼り付け shellcheck.netライン19のための問題、このフィードバックを:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

回答:


244
echo -e $lines | while read line 
    ...
done

whileループはサブシェルで実行されます。したがって、変数に加えた変更は、サブシェルが終了すると使用できなくなります。

代わりに、ヒア文字列を使用して、whileループをメインシェルプロセスに再書き込みできます。echo -e $linesサブシェルでのみ実行されます:

while read line
do
    if [[ "$line" == "second line" ]]
    then
        foo=2
        echo "Variable \$foo updated to $foo inside if inside while loop"
    fi
    echo "Value of \$foo in while loop body: $foo"
done <<< "$(echo -e "$lines")"

echo割り当てるときにすぐにバックスラッシュシーケンスを展開することで、上記のhere-stringのやや醜いものを取り除くことができますlines。そこで$'...'引用の形を使うことができます:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"

20
<<< "$(echo -e "$lines")"シンプルへの変更の改善<<< "$lines"
beliy 2017年

ソースがtail -f固定テキストではなくソースであった場合はどうなりますか?
mt eee 2018年

2
@mteeeを使用できますwhile read -r line; do echo "LINE: $line"; done < <(tail -f file)(明らかに、ループはからの入力を待ち続けるので終了しませんtail)。
PP

私は/ bin / shに制限されています-古いシェルで動作する別の方法はありますか?
user9645

1
@AvinashYadav問題はwhileループやforループに実際には関係ありません。むしろ、サブシェルすなわちの使用は、中cmd1 | cmd2cmd2サブシェルです。したがって、forループがサブシェルで実行されると、予期しない/問題のある動作が発生します。
PP

48

更新#2

説明はBlue Moonsの答えにあります。

代替ソリューション:

排除する echo

while read line; do
...
done <<EOT
first line
second line
third line
EOT

here-is-the-document内にエコーを追加します

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

echoバックグラウンドで実行:

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

明示的にファイルハンドルにリダイレクトします(< <!のスペースに注意してください)。

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

または単ににリダイレクトしますstdin

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

そして、chepner(除去するecho):

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

変数$linesは、新しいサブシェルを開始せずに配列に変換できます。文字\nを何らかの文字(たとえば、実際の改行文字)に変換し、IFS(内部フィールド区切り文字)変数を使用して文字列を配列要素に分割する必要があります。これは次のように行うことができます:

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

結果は

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")

lines変数の唯一の目的はwhileループをフィードすることであると思われるため、ヒアドキュメントの+1 。
chepner 2013年

@chepner:Thx!あなたに捧げる別のものを追加しました!
TrueY 2013年

ここにはさらに別の解決策がありますfor line in $(echo -e $lines); do ... done
dma_k

@dma_kコメントありがとうございます!このソリューションでは、1つの単語を含む6行が生成されます。OPのリクエストは異なりました
TrueY

賛成。here-isのサブシェルでエコーを実行することは、ashで機能する数少ないソリューションの1つでした
Hamy

9

あなたは、このbash FAQを尋ねる742342番目のユーザーです。答えは、パイプによって作成されたサブシェルで設定される変数の一般的なケースについても説明します。

E4)コマンドの出力をにパイプすると、読み取りコマンドの終了時にread variable出力が表示されないのはなぜ$variableですか?

これは、Unixプロセス間の親子関係に関係しています。これは、への単純な呼び出しだけでなく、パイプラインで実行されるすべてのコマンドに影響しますread。たとえば、コマンドの出力whileを繰り返し呼び出すループにパイプするreadと、同じ動作になります。

パイプラインの各要素は、組み込み関数やシェル関数であっても、パイプラインを実行するシェルの子である別個のプロセスで実行されます。サブプロセスは、その親の環境に影響を与えることはできません。ときにreadコマンドが入力に変数を設定し、その変数だけサブシェルではなく、親シェルに設定されています。サブシェルが終了すると、変数の値は失われます。

で終わる多くのパイプラインread variableは、指定されたコマンドの出力をキャプチャするコマンド置換に変換できます。次に、出力を変数に割り当てることができます。

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

に変換することができます

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

残念ながら、これは、複数の変数引数が指定されたときに読み取られるように、テキストを複数の変数に分割するようには機能しません。これを行う必要がある場合は、上記のコマンド置換を使用して出力を変数に読み込み、bashパターン削除拡張演算子を使用して変数を切り取るか、次のアプローチのバリアントを使用できます。

/ usr / local / bin / ipaddrが次のシェルスクリプトであるとします。

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

使用する代わりに

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

ローカルマシンのIPアドレスを別々のオクテットに分割するには、次を使用します。

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

ただし、これによりシェルの位置パラメータが変更されることに注意してください。それらが必要な場合は、保存する前に保存してください。

これが一般的なアプローチです。ほとんどの場合、$ IFSを別の値に設定する必要はありません。

その他のユーザー提供の代替手段には次のものがあります。

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

そして、プロセス置換が利用可能な場合、

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

7
Family Feudの問題を忘れました。時には、答えを書いた人と同じ単語の組み合わせを見つけるのが非常に難しいため、間違った結果が殺到したり、その答えを除外したりすることはありません。
Evi1M4chine

3

うーん...私はこれがオリジナルのBourneシェルで機能することをほぼ誓いますが、実行中のコピーにアクセスしてチェックすることはできません。

ただし、この問題には非常に簡単な回避策があります。

スクリプトの最初の行を次のように変更します。

#!/bin/bash

#!/bin/ksh

出来上がり!パイプラインの最後での読み取りは、Kornシェルがインストールされていることを前提として、問題なく機能します。


1

stderrを使用してループ内に格納し、それを外部から読み取ります。ここで、var iは最初に設定され、ループ内で1として読み取られます。

# 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

または、heredocメソッドを使用して、サブシェル内のコードの量を減らします。反復i値は、whileループの外で読み取ることができることに注意してください。

i=1
while read -r s;
do
  echo $s > /dev/null
  let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i

0

非常に簡単な方法はどうですか

    +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 here

    foo=0
    bar="hello"

    if [[ "$bar" == "hello" ]]
    then
        foo=1
        echo "Setting  \$foo to $foo"
    fi

    echo "Variable \$foo after if statement: $foo"

    lines="first line\nsecond line\nthird line"

    function my_while_loop
    {

    echo -e $lines | while read line
    do
        if [[ "$line" == "second line" ]]
        then
        foo=2; return 2;
        echo "Variable \$foo updated to $foo inside if inside while loop"
        fi

        echo -e $lines | while read line
do
    if [[ "$line" == "second line" ]]
    then
    foo=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 anyway
    echo "Value of \$foo in while loop body: $foo"

done
}

    my_while_loop; foo="$?"

    echo "Variable \$foo after while loop: $foo"


    Output:
    Setting  $foo 1
    Variable $foo after if statement: 1
    Value of $foo in while loop body: 1
    Variable $foo after while loop: 2

    bash --version

    GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
    Copyright (C) 2007 Free Software Foundation, Inc.

6
多分ここの表面の下にまともな答えがあるでしょう、しかしあなたはそれを試して読むのが不愉快であるほどにフォーマットを破壊しました。
Mark Amery 2016

元のコードを読むのが楽しいということですか?(私はついてきた:p)
Marcin

0

これは興味深い質問であり、Bourneシェルとサブシェルの非常に基本的な概念に触れています。ここでは、ある種のフィルタリングを行うことにより、以前のソリューションとは異なるソリューションを提供します。実生活で役立つ例を挙げます。これは、ダウンロードされたファイルが既知のチェックサムに準拠していることを確認するためのフラグメントです。チェックサムファイルは次のようになります(3行だけ表示)。

49174 36326 dna_align_feature.txt.gz
54757     1 dna.txt.gz
55409  9971 exon_transcript.txt.gz

シェルスクリプト:

#!/bin/sh

.....

failcnt=0 # this variable is only valid in the parent shell
#variable xx captures all the outputs from the while loop
xx=$(cat ${checkfile} | while read -r line; do
    num1=$(echo $line | awk '{print $1}')
    num2=$(echo $line | awk '{print $2}')
    fname=$(echo $line | awk '{print $3}')
    if [ -f "$fname" ]; then
        res=$(sum $fname)
        filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na == $1 && nb == $2) { print "TRUE"; } else { print "FALSE"; }}')
        if [ "$filegood" = "FALSE" ]; then
            failcnt=$(expr $failcnt + 1) # only in subshell
            echo "$fname BAD $failcnt"
        fi
    fi
done | tail -1) # I am only interested in the final result
# you can capture a whole bunch of texts and do further filtering
failcnt=${xx#* BAD } # I am only interested in the number
# this variable is in the parent shell
echo failcnt $failcnt
if [ $failcnt -gt 0 ]; then
    echo $failcnt files failed
else
    echo download successful
fi

親とサブシェルは、echoコマンドを介して通信します。親シェルの解析しやすいテキストを選択できます。この方法では、通常の考え方が崩れることはなく、後処理を行う必要があるだけです。そのために、grep、sed、awkなどを使用できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.