2006年09月12日
bash で,サブシェルが起動される条件
今回は少々マニアックというか,重箱の隅的お話です.
bash(1) には,複数のコマンドをまとめたり,コマンドの実行結果をコマンドラインに取り込むための記法が複数あります.それらのコマンドを実行するために,bash は必要に応じてサブシェルを起動しますが,どういう記述をした際にサブシェルが起動されるのか,いまいちはっきりしなかったため,実際に試してみました.今回試したのは,
( ), $( ), { }, <( )
です.
さて,今回サブシェルが立ち上がるか否かは,
ps --forest
を実行して,ps
コマンドの親プロセスがどれになっているかで確認しています.bash が設定する $PPID
変数を見ないのは,変数の展開をどのシェルがするかに依存するために,確認しにくいからです.ps
コマンドを --forest
オプション付きで単純に起動すると
$ ps --forest PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27975 pts/18 00:00:00 \_ psとなります.一番上の PID = 20218 の bash がターミナル上で動いている bash になります.サブシェルが起動される場合は,この bash と ps の間に,もう一つ bash が動くはずです.
- ( )
$ (ps --forest) PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27976 pts/18 00:00:00 \_ psとなって,サブシェルは起動されません.しかしコマンドをもう一つ追加してやると
$ (ps --forest; echo -n) PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27977 pts/18 00:00:00 \_ bash 27978 pts/18 00:00:00 \_ psとなって,サブシェルが起動されます.次の二つのパターンは,
{ }
のケースとの比較用です.( )
ではパイプ接続されても結果に影響はないようです.
$ (ps --forest) | cat PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27979 pts/18 00:00:00 \_ ps 27980 pts/18 00:00:00 \_ cat $ echo | (ps --forest) PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27982 pts/18 00:00:00 \_ ps
- $( )
$( )
は,指定されたコマンドの実行結果をコマンドライン上に差し込むためのものです ` `
と機能的には同じです.出力結果を表示するために echo
を使っています.$( )
全体を " でくくっているのは,改行が保存されるようにするためです.結果は次のようになりました.パターン的には ( )
と同じです.
$ echo "$(ps --forest)" PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27983 pts/18 00:00:00 \_ ps $ echo "$(ps --forest; echo -n)" PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27984 pts/18 00:00:00 \_ bash 27985 pts/18 00:00:00 \_ ps
- { }
{ }
はブロックを作るための構文です.コマンドが一つの場合は
$ { ps --forest; } PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27986 pts/18 00:00:00 \_ psとなります.コマンドを二つにした場合
$ { ps --forest; echo -n; } PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27987 pts/18 00:00:00 \_ psとなって,前の
( )
や $( )
と違いサブシェルは起動されません.では,ブロックの前後にパイプを挟んでやるとどうなるでしょうか.
$ { ps --forest; } | cat PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27988 pts/18 00:00:00 \_ bash 27990 pts/18 00:00:00 | \_ ps 27989 pts/18 00:00:00 \_ cat $ echo | { ps --forest; } PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27992 pts/18 00:00:00 \_ bash 27993 pts/18 00:00:00 \_ psのように,やっぱり
( )
や $( ) の場合と違い,今度はコマンドが一つだけでもサブシェルが起動されるようになってしまいました.この違いは,当然ながらシェル変数の変更を伴う場合に影響を及ぼします.つまり,このようなことが起こります.
$ a=aaa $ { a=bbb; } $ echo $a bbb $ echo | { a=ccc; } $ echo $a bbbちなみに,この関係は
while
や for
などの制御文のブロックでも同様に発生します.
$ check=true; while $check; do ps --forest; check=false; done PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30458 pts/9 00:00:00 \_ ps $ check=true; while $check; do ps --forest; check=false; done | cat PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30459 pts/9 00:00:00 \_ bash 30461 pts/9 00:00:00 | \_ ps 30460 pts/9 00:00:00 \_ cat $ check=true; echo | while $check; do ps --forest; check=false; done PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30463 pts/9 00:00:00 \_ bash 30464 pts/9 00:00:00 \_ ps $ for i in 1; do ps --forest; done PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30466 pts/9 00:00:00 \_ ps $ for i in 1; do ps --forest; done | cat PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30467 pts/9 00:00:00 \_ bash 30469 pts/9 00:00:00 | \_ ps 30468 pts/9 00:00:00 \_ cat $ echo | for i in 1; do ps --forest; done PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30471 pts/9 00:00:00 \_ bash 30472 pts/9 00:00:00 \_ ps関数でも同じです.
$ testf(){ ps --forest; } $ testf PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30478 pts/9 00:00:00 \_ ps $ testf | cat PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30479 pts/9 00:00:00 \_ bash 30481 pts/9 00:00:00 | \_ ps 30480 pts/9 00:00:00 \_ cat $ echo | testf PID TTY TIME CMD 29996 pts/9 00:00:00 bash 30483 pts/9 00:00:00 \_ bash 30484 pts/9 00:00:00 \_ ps
- <$( )
$ less <(ssh dokoka cat hoge)のような形で使います.つまり,コマンドの実行結果を名前付きパイプを通して取り出すことができるようにする構文です.リモートのホストのファイルとの diff を取るのにも便利で重宝してます.さて結果がどうなるかですが,
$ cat <(ps --forest) PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27994 pts/18 00:00:00 \_ bash 27995 pts/18 00:00:00 | \_ ps 27996 pts/18 00:00:00 \_ cat $ cat <(ps --forest; echo -n) PID TTY TIME CMD 20218 pts/18 00:00:00 bash 27997 pts/18 00:00:00 \_ bash 27998 pts/18 00:00:00 | \_ ps 27999 pts/18 00:00:00 \_ catとなりました.つまり,必ずサブシェルが起動されます.
klab_gijutsu2 at 20:33│Comments(6)│TrackBack(0)
トラックバックURL
この記事へのコメント
1. Posted by _goma 2006年09月15日 10:54
こんにちは
これ、わかってないとはまりますよね。
v="old";(v="new";echo $v);echo $v
$vは変わんないですね。
私はreadでよくはまってました。
v="old";VV="old"
echo -e "a\nc\nd" | while read v ;do
VV=$v
echo $v $VV
done
echo $v $VV
これ、わかってないとはまりますよね。
v="old";(v="new";echo $v);echo $v
$vは変わんないですね。
私はreadでよくはまってました。
v="old";VV="old"
echo -e "a\nc\nd" | while read v ;do
VV=$v
echo $v $VV
done
echo $v $VV
2. Posted by かつみ 2006年09月15日 11:29
while read はよく使いますよね.私もしばらく悩んだことがあります.これは,ちょっと強引ですがこういう形にすれば,サブシェルが起動されずに済みます.
v="old";VV="old"
while read v ;do
VV=$v
echo $v $VV
done < <(echo -e "a\nc\nd")
echo $v $VV
ちなみに,本文では ( ) の中のコマンドが一つだけの時はサブシェルは起動されないと書きましたが,
v="old";(v="new");echo $v
でもやっぱり v の値は old のままだったりします.bash(1) によれば,( ) の中身はサブシェルで実行する,と書いてますので,それに合わせてるのだと思います.
v="old";VV="old"
while read v ;do
VV=$v
echo $v $VV
done < <(echo -e "a\nc\nd")
echo $v $VV
ちなみに,本文では ( ) の中のコマンドが一つだけの時はサブシェルは起動されないと書きましたが,
v="old";(v="new");echo $v
でもやっぱり v の値は old のままだったりします.bash(1) によれば,( ) の中身はサブシェルで実行する,と書いてますので,それに合わせてるのだと思います.
3. Posted by _goma 2006年09月15日 14:26
なるほど。勉強になります。
4. Posted by fumiyas 2011年06月09日 23:08
「本文では ( ) の中のコマンドが一つだけの時はサブシェルは起動されない」は間違いじゃないでしょうか。Linux で bash 4.1.5 を strace(8) すると clone(2) しているし、Solaris で bash 3.00.16 を truss(1) すると fork1(2) しています。
()、$()、<() >() と | (パイプ)の右辺(?)は別プロセスで実行されることを知っていれば「シェル変数を変更したのに反映されない?!」なんてミスはなくなるかと。
()、$()、<() >() と | (パイプ)の右辺(?)は別プロセスで実行されることを知っていれば「シェル変数を変更したのに反映されない?!」なんてミスはなくなるかと。
5. Posted by fumiyas 2011年06月09日 23:12
ちなみに ksh, zsh は | パイプの左辺が子プロセスで実行されます。
なので以下を ksh, zsh で実行すると「foo」が表示されます。
echo foo |read bar
echo $bar
なので以下を ksh, zsh で実行すると「foo」が表示されます。
echo foo |read bar
echo $bar
6. Posted by かつみ 2011年06月10日 11:48
strace で見ると、確かに ( ) でも、サブシェルが起動されてますね。 ps コマンドで親プロセスを確認した結果にとらわれすぎていたようです…
(ps --forest) の時の ps と親のシェルプロセスが直接の親子関係に見えたのは、( ) の中のコマンド(記事の例では ps)が1つだけの時は、サブシェルが fork() & exec() ではなく、サブシェルが直接 ps を exec() しているのでしょうね…
(ps --forest) の時の ps と親のシェルプロセスが直接の親子関係に見えたのは、( ) の中のコマンド(記事の例では ps)が1つだけの時は、サブシェルが fork() & exec() ではなく、サブシェルが直接 ps を exec() しているのでしょうね…