コマンドラインシェルのマニアックな比較
yash の実装の参考にするために、各シェルの細かな挙動の違いに付いて調べた結果を書いておく。
まずはプロセスツリーのできかた。bash (バージョン 3.1.17(1)) で cat | cat | cat を実行し、その状態を他の端末で ps fj して観察した。(なお、ps コマンドに f を付けるとプロセスツリーが見られるのは多分 GNU/Linux だけ)
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3997 5043 5043 3997 pts/1 5068 S 500 0:00 \_ bash 5043 5068 5068 3997 pts/1 5068 S+ 500 0:00 \_ cat 5043 5069 5068 3997 pts/1 5068 S+ 500 0:00 \_ cat 5043 5070 5068 3997 pts/1 5068 S+ 500 0:00 \_ cat
bash から三つの cat が子プロセスとして分岐している。まあ、これは普通だね。(注: この記事では Unix における分岐 (fork) と変身 (exec) についての解説はしないので云々)
tcsh (バージョン 6.14.00), zsh (バージョン 4.2.6) でも同じツリーができた。
3997 5017 5017 3997 pts/1 5036 S 500 0:00 \_ -csh 5017 5036 5036 3997 pts/1 5036 S+ 500 0:00 \_ cat 5017 5037 5036 3997 pts/1 5036 S+ 500 0:00 \_ cat 5017 5038 5036 3997 pts/1 5036 S+ 500 0:00 \_ cat3997 5101 5101 3997 pts/1 5117 S 500 0:00 \_ zsh 5101 5117 5117 3997 pts/1 5117 S+ 500 0:00 \_ cat 5101 5118 5117 3997 pts/1 5117 S+ 500 0:00 \_ cat 5101 5119 5117 3997 pts/1 5117 S+ 500 0:00 \_ cat
ところが ksh (ksh93) でも同じようになると思ったら違った。
3997 5204 5204 3997 pts/1 5205 S 500 0:00 \_ ksh 5204 5205 5205 3997 pts/1 5205 S+ 500 0:00 \_ ksh 5204 5206 5205 3997 pts/1 5205 S+ 500 0:00 \_ ksh
ksh では cat は組込みコマンドなのだ。よってプロセス名は cat ではなく ksh のままになっている。そして三つ目の cat は子プロセスに分岐せずに直接元のシェルが実行している。この方式では子プロセスが一つ少なくて済む分経済的だが、特殊文字 susp (普通は Ctrl-Z で入力できる) で SIGTSTP シグナルを送ると、子プロセスは停止するが元のシェルは cat 動作を続けたままでコマンドラインに戻らないので嵌まる。
続いて (cat /dev/stdin | cat | cat -)。コマンドを括弧で囲むことで、コマンドは元のシェルではなくサブシェルで実行されることになる。cat の引数に /dev/stdin と - が付いているのは ps の出力結果で三つの cat を区別しやすくするため。
まずは bash。元のシェルからサブシェルが分岐し、さらに三つの cat が分岐しているのが分かる。これも普通だね。
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3997 5467 5467 3997 pts/1 5492 S 500 0:00 \_ bash 5467 5492 5492 3997 pts/1 5492 S+ 500 0:00 \_ bash 5492 5493 5492 3997 pts/1 5492 S+ 500 0:00 \_ cat /dev/st 5492 5494 5492 3997 pts/1 5492 S+ 500 0:00 \_ cat 5492 5495 5492 3997 pts/1 5492 S+ 500 0:00 \_ cat -
続いて tcsh と zsh。サブシェルが分岐し、そこから最初の二つの cat が分岐するのは bash と同じだが、三つめの cat はサブシェルから分岐するのではなくて直接サブシェルが変身している。よってプロセスツリーは bash に比べてやや奇妙な形になる。
3997 5438 5438 3997 pts/1 5457 S 500 0:00 \_ -csh 5438 5457 5457 3997 pts/1 5457 S+ 500 0:00 \_ cat - 5457 5458 5457 3997 pts/1 5457 S+ 500 0:00 \_ cat /dev/st 5457 5459 5457 3997 pts/1 5457 S+ 500 0:00 \_ cat3997 5500 5500 3997 pts/1 5516 S 500 0:00 \_ zsh 5500 5516 5516 3997 pts/1 5516 S+ 500 0:00 \_ cat - 5516 5517 5516 3997 pts/1 5516 S+ 500 0:00 \_ cat /dev/st 5516 5518 5516 3997 pts/1 5516 S+ 500 0:00 \_ cat
最後に ksh の場合。なんとサブシェルを使わない場合と同じツリーになっている。
3997 5428 5428 3997 pts/1 5429 S 500 0:00 \_ ksh 5428 5429 5429 3997 pts/1 5429 S+ 500 0:00 \_ ksh 5428 5430 5429 3997 pts/1 5429 S+ 500 0:00 \_ ksh
サブシェルが分岐しない分 tcsh や zsh よりもさらに経済的だが、サブシェルは元のシェルとは独立した環境で動くという決まりはどうなるのだろうか。試しに ksh に (cd / ; cat /dev/stdin | cat | cat -) を実行させてみると、やはり同じプロセスツリーになった。
3997 5709 5709 3997 pts/1 5729 S 500 0:00 \_ ksh 5709 5729 5729 3997 pts/1 5729 S+ 500 0:00 \_ ksh 5709 5730 5729 3997 pts/1 5729 S+ 500 0:00 \_ ksh
サブシェルで作業ディレクトリを変更した後に cat を起動しているので、cat は新しい作業ディレクトリの元で動作しなければならないが、一方で元のシェルの作業ディレクトリは元のままでなければいけない。ということは、ksh はサブシェルの処理を始める段階で元のシェル環境を覚えておいて、サブシェルの処理が終わったら元の環境を復元するという、かなり面倒な作業をしていることになる。
次に試したコマンドは (echo ok | (sleep 10; cat) | cat) である。二つ目の cat はすぐに起動するが、一つ目のは 10 秒経たないと起動しないことに注意されたい。以下に示すプロセスツリーは、sleep が実行中の状態、すなわち一つ目の cat が起動する前の状態のものである。
まずは bash での結果。一つ目のサブシェルから二つ目のサブシェルが分岐し、さらにそこから sleep が分岐している。また、(二つ目の) cat が一つ目のサブシェルから分岐している。echo は ok を出力した後もうやることがないので、既に終了してツリー上には存在しない。ということで、これまた非常に普通なプロセスツリーである。
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND 3997 5875 5875 3997 pts/1 5910 S 500 0:00 \_ bash 5875 5910 5910 3997 pts/1 5910 S+ 500 0:00 \_ bash 5910 5912 5910 3997 pts/1 5910 S+ 500 0:00 \_ bash 5912 5913 5910 3997 pts/1 5910 S+ 500 0:00 | \_ sleep 1 5910 5914 5910 3997 pts/1 5910 S+ 500 0:00 \_ cat
続いては zsh での結果。一つ目のサブシェルから二つ目のサブシェルが分岐し、さらにそこから sleep が分岐しているのは bash と同じ。二つ目の cat は一つ目のサブシェルから分岐するのではなく一つ目のサブシェルが cat に変身している。その結果このような奇妙な形のツリーとなる。
3997 5922 5922 3997 pts/1 5939 S 500 0:00 \_ zsh 5922 5939 5939 3997 pts/1 5939 S+ 500 0:00 \_ cat 5939 5941 5939 3997 pts/1 5939 S+ 500 0:00 \_ zsh 5941 5942 5939 3997 pts/1 5939 S+ 500 0:00 \_ sleep 1
tcsh も zsh と同じ動作だが、echo (これは tcsh の組込みコマンドなので下のプロセスツリーには tcsh と表示されている) を行って終了したプロセスがゾンビプロセスとして残ったままになっている。
3997 5841 5841 3997 pts/1 5860 S 500 0:00 \_ -csh 5841 5860 5860 3997 pts/1 5860 S+ 500 0:00 \_ cat 5860 5861 5860 3997 pts/1 5860 Z+ 500 0:00 \_ [tcsh] <defunct> 5860 5862 5860 3997 pts/1 5860 S+ 500 0:00 \_ -csh 5862 5863 5860 3997 pts/1 5860 S+ 500 0:00 \_ sleep 10
ksh では cat, echo, sleep 全てが組込みコマンドであり、さらに一つ目のサブシェルは分岐せずに元のシェルがそのまま一つ目のサブシェルの処理を行うので (さすがに二つ目のサブシェルの方は分岐する)、プロセスツリーはもっと奇妙な形になる。
3997 5830 5830 3997 pts/1 5831 S 500 0:00 \_ ksh 5830 5832 5831 3997 pts/1 5831 S+ 500 0:00 \_ ksh
元のシェルが二つ目の cat の処理を行っており、分岐したサブシェルが sleep と一つ目の cat を行っている。プロセス数は極限まで最少化されているが、しかし SIGTSTP を送るとやはり嵌まる。
以上でプロセスツリーの話は終わり。次は echo ok | read var; echo $var で変数 var に文字列 ok を設定できるかどうかを調べた。結果は、ksh と zsh ではできたが tcsh と bash ではできなかった。(注: tcsh には read コマンドがない。代わりに echo ok | set var=$<; echo $var を実行した)
read コマンドを分岐したサブシェルで実行してしまうと、元のシェルではなくサブシェルに変数が設定されてしまう。括弧を使っていないのになぜサブシェルが出てくるのかと思う人がいるかもしれないが、bash では | でパイプを作った場合は組込みコマンドであるかどうかにかかわらず全てのコマンドがそれぞれ分岐した子プロセスで実行される。tcsh では分岐するかどうかの条件はもっと複雑なようだ。
普通にコマンドを単独で実行する場合は、組込みコマンドならシェル内で直接処理し、それ以外のコマンドは分岐した子プロセスで実行するというのはどのシェルでも同じ。しかしパイプの中で実行する場合は、各コマンドの入出力を繋ぎ変える必要があり、また SIGTSTP や SIGINT シグナルを受け取った時の処理も複雑になるので、組込みコマンドを元のシェルで実行するのはかなり難しい処理になる。
なお、ksh と zsh では cat | read var でも変数 var に値を設定することができる。しかし ksh でこのコマンドを実行中に SIGTSTP を送るとやはり嵌まる。zsh では、SIGTSTP を送っただけではコマンドラインに戻らないので同じく嵌まったように見えるが、続けて SIGINT シグナルを送る (普通は Ctrl-C キー) と read コマンドが終了するのでコマンドラインに戻れる。ただしその後 fg しても cat しか復活しない (read コマンドは SIGINT によって終了しているので) ので嬉しくない。
最後にパラメータ展開の挙動について。どのような場合に *
がファイル名展開の対象となるのかを調べた。参考までにそのうち公開予定の yash 1.0β0 の挙動も示しておく。
以下、変数 var には文字列 * が設定してあり、また変数 x は存在しないものとする。また、bash* と zsh* はそれぞれ bash, zsh を sh として起動したときの動作であり、yash* は --posix オプション付きで起動したときの動作である。
コマンド | ksh | bash | bash* | zsh | zsh* | yash | yash* |
---|---|---|---|---|---|---|---|
echo "$var" | * | * | * | * | * | * | * |
echo $var | 展開 | 展開 | 展開 | * | 展開 | 展開 | 展開 |
echo "${x-"*"}" | 展開 | * | * | * | * | * | 展開 |
echo "${x-*}" | * | * | * | * | * | * | * |
echo ${x-"*"} | * | * | * | * | 展開 | * | * |
echo ${x-*} | 展開 | 展開 | 展開 | 展開 | 展開 | 展開 | 展開 |
ksh と bash* と zsh* とで挙動が異なるのが興味深い。echo ${x-${x-"*"}} のようにもっと複雑な事をすると、さらにわけの分からないことになる。
| 固定リンク
コメント