« yash 1.0β | トップページ | コマンドラインシェルのマニアックな比較 その 2 »

2007年12月31日 (月)

コマンドラインシェルのマニアックな比較

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      \_ cat
 3997  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          \_ cat
 3997  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*yashyash*
echo "$var" * * * * * * *
echo $var 展開展開 展開 * 展開展開展開
echo "${x-"*"}"展開* * * * * 展開
echo "${x-*}" * * * * * * *
echo ${x-"*"} * * * * 展開* *
echo ${x-*} 展開展開 展開 展開展開展開展開

ksh と bash* と zsh* とで挙動が異なるのが興味深い。echo ${x-${x-"*"}} のようにもっと複雑な事をすると、さらにわけの分からないことになる。

|

« yash 1.0β | トップページ | コマンドラインシェルのマニアックな比較 その 2 »

コメント

コメントを書く



(ウェブ上には掲載しません)




トラックバック


この記事へのトラックバック一覧です: コマンドラインシェルのマニアックな比較:

« yash 1.0β | トップページ | コマンドラインシェルのマニアックな比較 その 2 »