[Subject Prev][Subject Next][Thread Prev][Thread Next][Subject Index][Thread Index]

[linux-users:96159] Re: [bash]whileループ内変数値の扱いについて教えてください。


 しらいです。

In Message-Id <200211080816.AA00040 _at_ dde086n.cp.star-micronics.co.jp>
        Kenichi Nagai <nagai _at_ cp.star-micronics.co.jp>さんwrites:
> 永井@Linux初心者です。

> bashのwhileループについて教えてください。
> cat等のコマンド結果をwhileループに渡して処理した場合、whileループ中
> でカウントアップされたシェル変数値が、ループを抜けた後にうまく反映さ
> れません。

 それは while のせいではなくパイプ (|) のせいです。「|」で
多段に接続された各々のコマンドは別プロセスとして動きますので、
そのいずれのプロセスも親である shell に変数を渡すことは出来
ません。
 MS-DOS のように中間ファイルによって実現されている疑似パイ
プと異なり、UNIX の pipe(2) によって実現されているパイプの場
合、前段の実行が完了しなくても後段の実行が始まります。
 これは個々のプロセスが親である shell とは独立して動いてい
ることを意味しますので、その while は sub shell と呼ばれる子
プロセスによって実行されている訳です。
 shell 変数は一つのプロセス内でしか共有出来ませんから、その
内容を他のプロセスから見ることは不可能です。ファイルに吐き出
すか、もしくはパイプ以外の手段を講じるかですね。

 bash の場合、Bourne shell と異なりこの辺りの些末な仕様が異
なります。
 例えば、
	パイプの最後段が単純な外部コマンドだった場合:
		Bourne shell -> sub shell の子
		bash -> shell 自身の子
	while が redirect されていた場合:
		Bourne shell -> while は sub shell
		bash -> while は shell 自身
といったような仕様になっています。
 bash の場合、極力 sub shell を作らないように心掛けているん
でしょうね。この特徴を使うと、while に渡す入力を cat からの
パイプではなく redirectee 「<」を使って渡せば sub shell 化し
なくて済むことになります。

	cnt=0
	while read LINE
	do
	  echo "$cnt: $LINE"
	  cnt=`expr $cnt + 1`
	done < $0
	echo "cnt(after LOOP) = $cnt"

	0: cnt=0
	1: while read LINE
	2: do
	3: echo "$cnt: $LINE"
	4: cnt=`expr $cnt + 1`
	5: done < $0
	6: echo "cnt(after LOOP) = $cnt"
	cnt(after LOOP) = 7


 但し、bash の特徴を使っているために、この手法は他の Bourne
shell 互換 shell では使えません。bash は Bourne shell 系の眷
族の中でもかなり異端な部類に属しますので、そういう shell に
依存した実装は良くないかも知れません。
 となると、shell 変数以外の入出力を使ってプロセス間でのデー
タ授受を行なうことが必要です。簡単なのは標準入出力を使う実装
でしょう。

	while_loop='
	  while read LINE || { echo $cnt && false; };
	  do
	    echo 1>&3 3>&- "$cnt: $LINE";
	    cnt=`expr $cnt + 1`;
	  done
	'
	cnt=0
	exec 3>&1
	cnt=`cat $0 | eval $while_loop`
	echo "cnt(after LOOP) = $cnt"

	0: while_loop='
	1: while read LINE || { echo $cnt && false; };
	2: do
	3: echo 1>&3 3>&- "$cnt: $LINE";
	4: cnt=`expr $cnt + 1`;
	5: done
	6: '
	7: cnt=0
	8: exec 3>&1
	9: cnt=`cat $0 | eval $while_loop`
	10: echo "cnt(after LOOP) = $cnt"

 ここでは幾つかの TIPS を使っています。主だったところを紹介
しましょう。

1. 標準出力の複製
 while ステートメントの標準出力を `` で拾って変数値を得よう
としていますので、while の中から標準出力に吐き出すと、その文
字列まで一緒に変数値として取込まれてしまいます。
 そうならないために、`` の実行前に予め標準出力の複製を作っ
ておきます。ここでは file descriptor #3 に標準出力 #1 をコピ
ーしています。(line 8.)
 こうしておけば、while の中でもこの #3 に対して出力してやれ
ば、元々の標準出力に対する出力になります。(line 3.)

2. while ステートメントのブロック化
 残念なことに、この while ステートメントはその中で `` を使
ってしまっています。`` は入れ子にすることが出来ないので、ど
こかでこの while ステートメントだけをブロック化しておく必要
があります。
 そのための手段は幾つかありますが、ここではステートメント全
体を変数に代入し、後でそれを eval で評価する手法を採りました。
この手法が一番汎用性があります。(line 9.)
 汎用性の低い方法としては、他に以下のようなものがあります。
	・`` の代わりに入れ子可能な $() を使う。
	→ bash 依存 (POSIX 規格にはある)
	・while ステートメントを関数化する。
	→ Bourne shell 依存 (古い実装では関数が使用不可)

3. 改行位置に「;」を追加
 while ステートメントを '' で括って文字列化してしまいました
ので、改行が単なるフィールドセパレータとして扱われてしまいま
す。空白やタブと同列の存在です。
 コマンドリストのセパレータには改行以外に「;」が使えますの
で、元々の改行位置に「;」を追加しました。(line 1.-4.)

# 「do」の後ろには元々セパレータは要りません。

4. read 完了時に $cnt を出力させる
 while ステートメントの中で $cnt を出力させるには、ループの
中で毎回出力させる方法もありますが、これだと最終的な $cnt の
値を取得するのに面倒な手順を必要とします。
 read が失敗した時点ですぐに while を抜けないように、その後
ろに選言条件で echo $cnt を付けました。これで、read に失敗し
た時、即ちループ終了時に $cnt が出力されます。
 但し、それだけだとループが終わらなくなってしまうので、echo
の後に false で無理矢理失敗させています。(line 2.)

# この {} によるグループ化は単純に条件演算の優先順位による
#ものです。別に () で括っても構いませんが、shell 文法の ()
#はグループ化ではなくて sub shell 化を意味します。


> テスト用に、w_test_sh を作成し、実行した結果が、result.txtです。
> w_test_sh中の最初のブロックでは、while loopを回しています。
> ループの中でcnt変数をインクリメントして、ループを抜けた後にcnt変数を
> 表示させるとちゃんと"5"になっています。

 ステートメントが実行中の shell の中で閉じて実行される分に
は、このように変数値も取得出来ます。どこで sub shell 化する
かについてはマニュアルでも触れられていないので判断が難しいで
しょうね。
 私も自分で shell を実装するようになって、さんざん調べ廻っ
た結果得た知識が多いです。やってみないと判らないことが多いの
で、なかなか難しいでしょうね。
 最近だと Bourne shell そのものの source が参照可能になって
いるのですが、あの source は BASIC ライクに記述されているの
で私には解析不能です。

 そういった実装の揺れを解消すべく、POSIX 規格が整備されてき
ているんだと思いますが、あの中では大まかな仕様しか記述されて
いないので、こういった些末な挙動については自分で一つ一つ地道
に調べていくしかないんでしょうかね。

                                               しらい たかし

この情報があなたの探していたものかどうか選択してください。
yes/まさにこれだ!   no/違うなぁ   part/一部見つかった   try/これで試してみる

あなたが探していた情報はどのようなことか、ご自由に記入下さい。特に「まさにこれだ!」と言う場合は記入をお願いします。
例:「複数のマシンからCATV経由でipmasqueradeを利用してWebを参照したい場合の設定について」
Follow-Ups: References: