連載
» 2012年09月14日 00時00分 公開

スマートな紳士のためのシェルスクリプト(8):シェルスクリプト最大の罠、while問題 (2/2)

[後藤大地,BSDコンサルティング株式会社]
前のページへ 1|2       

プロセスの関係をチェック

 もっと簡単な方法でプロセスがどういった関係になっているのかを調べてみよう。例えば次のように、whileの中でsleep(1)を使って処理を停止するようにする。

#!/bin/sh
OS=FreeBSD
while :
do
        OS=Linux
        sleep 100
        break
done
echo ${OS}

 この状態でプロセスの親子関係を表示させると、次のようになる。while04.shというのが上記のファイルだ。シェルスクリプトからsleep(1)が実行されていることが確認できる。

$ ps d | less 
 PID TT  STAT     TIME COMMAND
...略...
2446  3  S+    0:00.00 - /bin/sh ./while04.sh
2447  3  S+    0:00.00 `-- sleep 100
...略...
$ 

 パイプを挟んだwhileも同様にsleep(1)を挟んで、ps(1)で出力をチェックする。

#!/bin/sh
OS=FreeBSD
echo |
while :
do
        OS=Linux
        sleep 100
        break
done
echo ${OS}

 次のようになる。while03.shが上記ファイルだ。

% ps d | less  
 PID TT  STAT     TIME COMMAND
...略...
2429  3  S+    0:00.00 - /bin/sh ./while03.sh
2431  3  S+    0:00.00 `-- /bin/sh ./while03.sh
2432  3  S+    0:00.00   `-- sleep 100
...略...
$ 

 while03.shからwhile03.shが派生し、そこからsleep(1)が実行されている。サブシェルをfork(2)し、fork(2)した先でsleep(1)が実行されていることが分かる。

 一見するとwhileの外と中は同じレベルにあって、変数のスコープも同一であるかのような印象を受ける。しかし、パイプが挟まった時点ですでに別のプロセスになっており、変数はまったく共有できないことを知っておく必要がある。これがwhile問題だ。

回避方法あれこれ

 例えば、テキストファイルからBSDという単語が含まれている行を抜き出し、連番で表示して、最後に総数を出力するコマンドをシェルスクリプトで書くとする。while問題を理解していないケースでは、次のように変数を使ってスクリプトを組んだりしないだろうか。手続き型のプログラミング言語に慣れているなら、ごく自然な処理だ。

#!/bin/sh
i=0
grep BSD "${1}"         |
while read b
do
        i=$((i+1))
        echo "$i        $b"
done
echo "total lines : $i"

 このコマンドを実行すると次のようになる。

$ ./bsdcount01.sh /COPYRIGHT 
1       # $FreeBSD: head/COPYRIGHT 229067 2011-12-31 04:38:04Z obrien $
2       The compilation of software known as FreeBSD is distributed under the
3       Copyright (c) 1992-2012 The FreeBSD Project. All rights reserved.
4       The 4.4BSD and 4.4BSD-Lite software is distributed under the following
5       All of the documentation and software included in the 4.4BSD and 4.4BSD-Lite
6       the second BSD Networking Software Release, from IEEE Std 1003.1-1988, IEEE
7       NOTE: The copyright of UC Berkeley's Berkeley Software Distribution ("BSD")
8       To All Licensees, Distributors of Any Version of BSD:
9       As you know, certain of the Berkeley Software Distribution ("BSD") source
10      foregoing paragraph of those BSD Unix files containing it is hereby deleted
total lines : 0
$ 

 サブシェルとしてfork(2)した先のwhileの変数iは、fork(2)した段階でコピーされるため、0が格納されている。これがインクリメントされ、行の最初に出力されるわけだが、すでに別プロセスに分離しているので、whileの中の変数iをいくら変更しようとも、元のシェルスクリプトの変数iには何の変更も行われない。このため、最後に元のシェルスクリプトで変数iを出力すると、0が出力される。

 こうした問題に陥らないようにする方法はいくつもある。汎用的に適用できて、多くのケースで推奨できる方法は、変数を使用せずに「ファイル」を使う方法だ。ファイルはグローバルな名前空間なので、別プロセスであっても使用できる。カウントした値を随時ファイルに書き出して、元のシェルスクリプトに戻ってきたらファイルの中身を出力するようにすればよい。

#!/bin/sh
tmp=/tmp/$$
i=0
grep BSD "${1}"         |
while read b
do
        i=$((i+1))
        echo $i > $tmp-count
        echo "$i        $b"
done
echo "total lines : $(cat $tmp-count)"
rm $tmp-*

 実行結果は次のようになる。想定していた通りの動作だ。データをファイルに出力する方法はシェルスクリプトプログラミングではとても有益なので、汎用的なテクニックとして身につけてしまった方がよいだろう。

$ ./bsdcount02.sh /COPYRIGHT
1       # $FreeBSD: head/COPYRIGHT 229067 2011-12-31 04:38:04Z obrien $
2       The compilation of software known as FreeBSD is distributed under the
3       Copyright (c) 1992-2012 The FreeBSD Project. All rights reserved.
4       The 4.4BSD and 4.4BSD-Lite software is distributed under the following
5       All of the documentation and software included in the 4.4BSD and 4.4BSD-Lite
6       the second BSD Networking Software Release, from IEEE Std 1003.1-1988, IEEE
7       NOTE: The copyright of UC Berkeley's Berkeley Software Distribution ("BSD")
8       To All Licensees, Distributors of Any Version of BSD:
9       As you know, certain of the Berkeley Software Distribution ("BSD") source
10      foregoing paragraph of those BSD Unix files containing it is hereby deleted
total lines : 10
$ 

 今回のような処理は、どちらかといえばawk(1)で処理すべき内容なので、次のようにすべてawk(1)で書いてしまうという方法もある。実のところ、こうしたストリーム指向のテキスト処理はawk(1)で事足りることがかなり多く、シェルスクリプトの習得と合わせてawk(1)も習得しておきたいところだ。

#!/bin/sh
awk 'BEGIN      { i=0; }
     /BSD/      { i+=1;
                  print i "     " $0 }
     END        { print "total lines : " i }' "$1"

 複合技として、次のように書くこともできる。BSDを含む行をあらかじめ一時ファイルに書き出しておき、seq(1)の出力とpaste(1)で接続するというものだ。

#!/bin/sh
tmp=/tmp/$$
grep BSD "$1" > $tmp-1
seq 1000 | paste - "$tmp-1" | grep -E -v '^[0-9]*       $'
echo "total lines : $(gyo $tmp-1)"
rm $tmp-*

 どのように書いてもよいが、whileが使われている場合には、自分が意図したのとは違う動きをしている可能性があるということを常に考えておきたい。

 最初はパイプを使わないでwhileを使っていたものが、途中からパイプを挟むようになって突然動作が変わった、というケースはシェルスクリプトプログラミングではよく発生する。whileそのものを使わないように思考を変えるか、シェルスクリプトでは必ずファイルに出力するといったようにコーディングの発想を転換しておくとよい。

著者プロフィール

後藤 大地(Twitter ID:@daichigoto)

BSDコンサルティング株式会社取締役。オングス代表取締役。@ITへの寄稿、MYCOMジャーナルにおけるニュース執筆のほか、アプリケーション開発やシステム構築、『改訂第二版 FreeBSDビギナーズバイブル』『D言語パーフェクトガイド』『UNIX本格マスター 基礎編〜Linux&FreeBSDを使いこなすための第一歩〜』など著書多数。


前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。