web-dev-qa-db-ja.com

入力を「読み取り」にパイピングすると、「読み取り中...」コンストラクトに入力された場合にのみ機能するのはなぜですか?

私はこのようなプログラム出力から環境変数への入力を読み取ろうとしました:

echo first second | read A B ; echo $A-$B 

結果は次のとおりです。

-

AとBは常に空です。サブシェルでパイプコマンドを実行するbashについて読み、基本的に入力をパイプで読み取らないようにします。ただし、次のとおりです。

echo first second | while read A B ; do echo $A-$B ; done

動作しているようで、結果は次のとおりです。

first-second

誰かがここの論理とは何かを説明できますか? while ... doneコンストラクト内のコマンドは、サブシェルではなく、echoと同じシェルで実際に実行されるのですか?

69
huoneusto

stdinに対してループを実行し、結果を変数に保存する方法

bash (および他の Shell も)の下で、|を使用して別のコマンドに何かをパイプすると、暗黙的にforkが作成されます。 =、現在のセッションの子であり、現在のセッションの環境に影響を与えないサブシェル。

したがって、この:

TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
  while read A B;do
      ((TOTAL+=A-B))
      printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
    done
echo final total: $TOTAL

期待した結果が得られません! :

  9 -   4 =    5 -> TOTAL=    5
  3 -   1 =    2 -> TOTAL=    7
 77 -   2 =   75 -> TOTAL=   82
 25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343
echo final total: $TOTAL
final total: 0

計算される場所[〜#〜] total [〜#〜]はメインスクリプトで再利用できませんでした。

フォークの反転

bashProcess SubstitutionHere DocumentsまたはHere Stringsを使用すると、フォークを逆にすることができます。

ここに文字列

read A B <<<"first second"
echo $A
first

echo $B
second

ヒアドキュメント

while read A B;do
    echo $A-$B
    C=$A-$B
  done << eodoc
first second
third fourth
eodoc
first-second
third-fourth

ループ外:

echo : $C
: third-fourth

ここにコマンド

TOTAL=0
while read A B;do
    ((TOTAL+=A-B))
    printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
  done < <(
    printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664
)
  9 -   4 =    5 -> TOTAL=    5
  3 -   1 =    2 -> TOTAL=    7
 77 -   2 =   75 -> TOTAL=   82
 25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343

# and finally out of loop:
echo $TOTAL
-343

これで、メインスクリプト$TOTALを使用できます。

コマンドリストへのパイプ

ただし、stdinに対してのみ作業する場合、forkに一種のスクリプトを作成できます。

printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | {
    TOTAL=0
    while read A B;do
        ((TOTAL+=A-B))
        printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
    done
    echo "Out of the loop total:" $TOTAL
  }

あげる:

  9 -   4 =    5 -> TOTAL=    5
  3 -   1 =    2 -> TOTAL=    7
 77 -   2 =   75 -> TOTAL=   82
 25 -  12 =   13 -> TOTAL=   95
226 - 664 = -438 -> TOTAL= -343
Out of the loop total: -343

注:$TOTALメインスクリプトで使用できませんでした(最後の右中括弧}の後)。

lastpipe bashオプションを使用する

@CharlesDuffyが正しく指摘したように、この動作を変更するために使用されるbashオプションがあります。ただし、このためには、最初にdisablejob controlにする必要があります。

shopt -s lastpipe           # Set *lastpipe* option
set +m                      # Disabling job control
TOTAL=0
printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 |
  while read A B;do
      ((TOTAL+=A-B))
      printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[A-B] $TOTAL
    done

  9 -   4 =    5 -> TOTAL= -338
  3 -   1 =    2 -> TOTAL= -336
 77 -   2 =   75 -> TOTAL= -261
 25 -  12 =   13 -> TOTAL= -248
226 - 664 = -438 -> TOTAL= -686

echo final total: $TOTAL
-343

これは動作しますが、これは標準ではなく、スクリプトを読みやすくするのに役立たないので、私は(個人的に)好きではありません。また、ジョブ制御を無効にすると、この動作にアクセスするのに費用がかかるようです。

注:ジョブ制御は、インタラクティブセッションでのみデフォルトで有効になります。したがって、通常のスクリプトではset +mは必要ありません。

そのため、スクリプトでset +mを忘れると、コンソールで実行する場合またはスクリプトで実行する場合に異なる動作が発生します。これにより、これを理解したりデバッグしたりするのが簡単になりません...

63
F. Hauri

まず、このパイプチェーンが実行されます:

echo first second | read A B

それから

echo $A-$B

なぜなら read A Bはサブシェルで実行され、AとBは失われます。これを行う場合:

echo first second | (read A B ; echo $A-$B)

両方のread A Bおよびecho $A-$Bは同じサブシェルで実行されます(bashのマンページを参照、(list)

21
pbhd

よりクリーンな回避策...

read -r a b < <(echo "$first $second")
echo "$a $b"

この方法では、読み取りはサブシェルで実行されません(サブシェルが終了するとすぐに変数がクリアされます)。代わりに、使用する変数は、親シェルから変数を自動的に継承するサブシェルにエコーされます。

16
immotus

あなたが見ているのはプロセス間の分離です:readはサブシェルで発生します-echoコマンドが後で発生するメインプロセスの変数を変更できない別のプロセスです。

パイプライン(A | B)通常、シェルのコンテキストで(同じプロセスで)実行されるビルトイン(readなど)の場合でも、暗黙的に各コンポーネントをサブシェル(個別のプロセス)に配置します。

「Piping into while」の場合の違いは幻想です。同じルールが適用されます:ループはパイプラインの後半であるため、サブシェルにありますが、wholeループはsameサブシェルにあるため、分離プロセスの適用されません。

1
Martin Kealey