1つの「while read」ループで変数がローカルになっているのに、一見似ているように見える別のループではないのはなぜですか?


25

$x以下のスニペットから異なる値を取得するのはなぜですか?

#!/bin/bash

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 
#    x=55 .. I'd expect this result

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 
#    x=1 .. but why?

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 
#    x=1  .. but why?

スタックオーバーフローに関する同様の投稿:whileループ内で変更された変数は記憶されません
コードフォレスター

回答:


26

正しい説明はjsbillingsgeekosaurによってすでに与えられていますが、それについてもう少し詳しく説明します。

bashを含むほとんどのシェルでは、パイプラインの各サイドがサブシェルで実行されるため、シェルの内部状態の変更(変数の設定など)はパイプラインのそのセグメントに限定されたままになります。サブシェルから取得できる唯一の情報は、サブシェルが出力するもの(標準出力およびその他のファイル記述子)とその終了コード(0〜255の数値)です。たとえば、次のスニペットは0を出力します。

a=0; a=1 | a=2; echo $a

ksh(pdksh / mkshバリアントではなく、AT&Tコードから派生したバリアント)およびzshでは、パイプラインの最後のアイテムが親シェルで実行されます。(POSIXは両方の動作を許可します。)したがって、上記のスニペットは2を出力します。

便利なイディオムは、whileループ(またはパイプラインの右側にあるものは何でも、ここではwhileループが実際に一般的です)の継続をパイプラインに含めることです。

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}

1
Gillesに感謝します。a = 1 | a = 2は非常に明確な画像を提供します。内部状態のローカリゼーションだけでなく、パイプラインは実際にはパイプを介して何も送信する必要はありません(終了コード(?)以外)。パイプへの興味深い洞察です...私はスクリプトを実行すること< <(locate -ber ^\.tag$)に成功しました、元のわずかに不明瞭な回答とギークサウルスとグレンジャックマンのコメットのおかげで。特にjsbillingsのフォローアップコメントで:)
Peter.O

関数にパイプしたように感じるので、いくつかの変数とテストをその中に移動しました。
アクエリアスパワー14

8

可変スコープの問題に直面しています。パイプの右側にあるwhileループで定義された変数は、独自のローカルスコープコンテキストを持ち、変数の変更はループの外側では見られません。whileループは基本的にシェル環境のCOPYを取得するサブシェルであり、環境への変更はシェルの最後で失われます。このStackOverflowの質問をご覧ください。

更新済み:whileループが独自のサブシェルであるという重要な事実を指摘するのを怠りましたが、これはパイプのエンドポイントであるため、回答で更新しました。


@jsbillings ...さて、最後の二つのスニペットを説明するが、それは最初に説明していない、ループ内のxセット$の値は、ように担持された55(「しばらく」ループの範囲を超えて)
Peter.O

5
@ fred.bear:whileループをサブシェルにスローするパイプラインの末尾として実行しています。
ギーコサウルス

2
これは、bashプロセスの置換が作用する場所です。の代わりにblah|blah|while read ...、次のことができますwhile read ...; done < <(blah|blah)
グレンジャックマン

1
@geekosaur:回答に含めなかった詳細を記入してくれてありがとう。
jsbillings

1
-1申し訳ありませんが、この答えは間違っています。シェルではなく多くのプログラミング言語でこの機能がどのように機能するかを説明します。下の@Gillesはそれを正しく理解しました。
jpc

6

以下のように他の回答で述べた修正は主シェルに見えるがない作られたので、パイプラインの部分は、サブシェルで実行します。

Bashのみを検討する場合、cmd | { stuff; more stuff; }構造に加えて2つの回避策があります。

  1. プロセス置換からの入力をリダイレクトします。

    while read var ; do x=55 ; done < <(echo fred)
    echo "$x"

    コマンドinの出力<(...)は、名前付きパイプのように表示されます。

  2. lastpipekshのようバッシュの作業を行い、メインシェルプロセスにおけるパイプラインの最後の部分を実行するオプション、。ただし、ジョブ制御が無効になっている場合にのみ機能します。つまり、対話型シェルでは機能しません。

    bash -c '
      shopt -s lastpipe
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

    または

    bash -O lastpipe -c '
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

もちろん、プロセス置換はkshとzshでもサポートされています。しかし、いずれにしてもメインシェルでパイプラインの最後の部分を実行するため、回避策として使用する必要はありません。


0
#!/bin/bash
set -x

# prepare test data.
mkdir -p ~/test_var_global
cd ~/test_var_global
echo "a"> core.1
echo "b"> core.2
echo "c"> core.3


var=0

coreFiles=$(find . -type f -name "core*")
while read -r file;
do
  # perform computations on $i
  ((var++))
done <<EOF
$coreFiles
EOF

echo $var

Result:
...
+ echo 3
3

動作します。

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