web-dev-qa-db-ja.com

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

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

#!/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)
151
Eric Lilja
echo -e $lines | while read line 
    ...
done

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

代わりに、 here文字列 を使用して、メインのシェルプロセスに入るように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"
194
P.P.

更新#2

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

代替ソリューション:

echoを削除します

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

こちらの文書の中にエコーを追加する

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(Internal Field Separator)変数を使用する必要があります。これは次のようにして行うことができます。

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")
45
TrueY

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

E4)コマンドの出力をread variableにパイプすると、readコマンドが終了したときに出力が$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)

これは、残念ながら、複数の変数の引数が与えられた場合のreadのように、テキストを複数の変数に分割するのには役立ちません。これを行う必要がある場合は、上記のコマンド置換を使用して出力を変数に読み込み、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))
9
Jens

うーん...これはオリジナルのBourne Shellでもうまくいったことを誓いますが、実行するためのコピーにアクセスすることはできません。

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

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

#!/bin/bash

#!/bin/ksh

Et voila! Kornシェルがインストールされていると仮定すると、パイプラインの最後の読み取りは問題なく動作します。

1
Jonathan Quist

とても簡単な方法はどうですか

    +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.
0
Marcin

これは興味深い質問で、Bourne ShellとSubshel​​lの非常に基本的な概念に触れます。ここでは、ある種のフィルタリングを行うことによって、以前のソリューションとは異なるソリューションを提供します。実生活で役立つかもしれない例を挙げます。これは、ダウンロードしたファイルが既知のチェックサムに準拠していることを確認するための断片です。チェックサムファイルは次のようになります(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などを使うことができます。

0
Kemin Zhou