先月からほそぼそと作っているシェルのv0.4.0をリリースした。
追加した機能としては
- リダイレクト機能
- コマンド制御演算子
&&
/||
/;' /
&`
- パイプ機能
cmd1 | cmd2
- ビルトインコマンド
builtin
/cd
/echo
/puts
- %記法
%Q
/%q
- bintestの追加
リダイレクト機能とO_CLOEXEC
まず最初に追加した機能はリダイレクト機能だった。
bashの仕様とソースを参考にして、実装したのだけどかなりの種類があってかなり大変だった…。
が、そのおかげでだいぶリダイレクト機能には詳しくなった。
リダイレクトの演算子は 先 演算子 元
となっていて、例えば 2>&1
はFD1を2に書き込み権限付きでdupするということなのだと、頭ではなく心で理解したッ!(何年*nix使ってるんだよ)
また、bashの場合 元
の方は内部的にwordというトークンで処理されているので例えば 2>& 1
みたいにスペースを大量に挟んでも構文エラーにならないし、 2>&1
したときと同じ挙動をするというのbashトリビア(?)を知った*1。
そんなこんなで、リダイレクト演算子をパーサーに組み込み、意図したFD・ファイルをopenできるようになったのだが、fork/execveしたプロセスに引き継がれない!という問題が発生した。 原因は、openしたFDにO_CLOEXECフラグが立っていることだった。 このフラグは、exec系のコマンドを呼び出すときにFDを閉じるという機能で、FDリークを防ぐために使われる。 mrubyではデフォルトでこのフラグを付与するために、forkしてexecveを呼び出すとFDが閉じられてしまうということであった。 今回は、意図的にFDリークをしたいのでopenしたFDのO_CLOEXECフラグを消すことで対処した(IO#close_on_exec = false)。
コマンド制御演算子
これもbashの実装を見ながら実装した。
bashのコード上ではconnectorという名前で実装されていた。
;' や
&` もconnectorとして実装されているのはなるほど感が高かった(bashのコードを読む前では、コマンドを分割しているのかと思っていた)。
reddish-shell的には通常のcommandも、connectorも同じ呼び出し規約にすることで透過に処理できるようにしたらいい感じに実装できたと思う。 …いまは、もろもろの関係でそうなっていないけど。
パイプ機能
シェルといえばコマンドの同士のパイプ。 シェルの実装の中でも実装していてかなり楽しい部類だと思う。 コマンド制御演算子を応用して実装したので、そこまで苦労せずに作れた。
ただし、コマンド制御演算子と違い順番にコマンドを実行するわけではなく、パイプでつないだコマンドを非同期的に実行する必要があり、少し工夫が必要だった。 また、このときはまだSIGINTのハンドリングをしていたのだが、ナイーブに実装するとパイプで一番はじめに実行したプロセスにしかシグナルが飛ばなかったりして、どうしたものかとなった(解決策については後述)。
ビルトインコマンド
いろいろな機能を実装し、動作テストしていてふと「(カレントディレクトリが変更できないのでは…?」ということに気が付き実装した。
cd や echo など普段何気なく使っている機能を改めてRubyで実装するのはなかなかおもしろい。
特に echo の -e
は何気にいろいろなエスケープ文字を使っていて、それらを実装するのがなかなか楽しかった。
ただ、Unicode周り(\u/\U/\u{})はかなり雑に実装してしまったので、意図しない動作をするかもしれない…。
echoの -e
を実装しているときに、Rubyの文字リテラルとして処理すればエスケープ文字の処理が楽なのでは?っと考えたのだが、Rubyが文字リテラルをエスケープ文字に変換(例えば "\e" を0x1bにする)する処理は、コードをパースしているときに行っていて "\e" (バックスラッシュとe)という文字を変数に入れても変換されないんだなという謎の学びを得た*2。
evalすればいいのだけど、それはそれで別の問題がおこるので正規表現を使って処理するようにした。
%記法
reddish-shellはRubyパワードなので、なにかしらRuby要素を入れたい!ということで%記法を実装した。 といっても、%Q/%qしか使えないw それ以外は、Rubyの文法上では便利なのだけど、シェルのコマンドラインとしてみるとあまり活用できないのでいまは実装していない。
bintest
そこそこの規模になってきたのでテストコードを追加した。 mrubyの場合testとbintestという2つの枠組みがあって、前者はmrbtestを使ったRubyコードでのテストで、後者は作成したバイナリを実行してテストするものになっている。 当初testを使おうと思っていたのだが、依存するmrbgemsのテストも実行され、特に古いコードの場合WARNが大量に起こってしまい*3、本来テストしたい部分以外で発生してしまうためやめてしまった*4。 テストツールは、通常のmrbgemと同じように作り、build_config.rbに定義を追加するだけで追加できるのでとても便利であった*5。
シグナルハンドリングの実装とmrubyでのスレッド
ここまでは、実装した機能について書いたが、ここからは今後実装したい機能について書く。 シェルとしてはまだま機能不足ではあるが、まぁ、それなりに動くようになったのだが、シグナルハンドリングを行っていないために、プロセスを強制停止できないという致命的な状態になっている。 とはいえ全く何も手を付けていないわけではなく、当初は@pyamaが作ったmruby-signal-threadを使ってSIGINTを補足してSIGINTを飛ばすようにしていた。
しかし、あるときからSEGVするようになってしまった。 ちゃんと調べていない&理解していないので間違っているかもしれないが、mrubyではスレッドを作るときにRuby VMを複製しているのだが、スレッドを作成した状況によってはコピーするオブジェクトが多すぎてスタックを壊してしまうようだった。 実際に、mruby-signal-threadをプログラムの最初で実行するとSEGVせず、プロセスをforkした直後に実行するとSEGVしてしまうのであった。
そのため、一旦この機能はオミットしている。 解決作としては、シグナルハンドリング部分をmrubyの範囲外で行うようにするというのを考えている。 具体的には、mrubyからはforkしたプロセスのpid情報を付与してシグナルハンドリングを行うスレッドを起動し、シグナルハンドリングを行うスレッドはkillを実行だけをする。 mrubyのコードではやらず、完全にC言語だけで実装することで先の問題を解決する。 手元ではすでに実装していて、動作もしているのだが、まだコミットはしていない。 この方法を使えば解決できそうなのが見えているのだが、mrubyの領域にフィードバックできないので、例えばbashのtrapコマンドとかの実装がかなり厳しいなという気持ちになっている…が、それを気にしていたら先に進まないのでいまは考えないようにしている。
シグナルハンドリングはうまく行ったのだが、パイプ機能のところでも書いたのだけど起動したプロセスの管理、いわゆる、ジョブコントロールが次の課題になっている。 PIDをナイーブに扱っているとかなり煩雑になることがわかってどうしようかなぁと悩やみつつ、bashのコードを読んでいたら、プロセスグループというのを知った。 どうつかうのかとかは全くわからんし、ネットにもあまり情報がないのでLinuxシステムプログラミング を買って勉強をしている。 プロセスグループ以外にも、シェルを作る上で便利な機能があって学びしかない。
*1:これ以外にあるけど割愛
*2:当たり前の話ではあるのだけど、 "どこで" 変換しているのかをあまり意識していなかった。今回の場合ソースコード上に "\e"と書いて、Rubyを実行させると実行時には0x1bとして解釈されるということを改めて認識したという話
*3:assertルールの変更とかに追随していないものがあったりする
*4:もちろん、OSSなのでPRしたりして直すのがよいのだが、こちらがメインになってしまいreddish-shellの実装がおろそかになりそうだったため実施していない(そのうちやりたい)))。
bintestでは、実際にreddish-shellにコマンドを流し込んで意図した動作をするかを確認している。 その中で例えばリダイレクトのテストでは、FDが正しく設定されているかを外から観測するのが難しいため、テストするためのツールをmrubyで作った((当初Rubyスクリプトで作っていたのだが、RubyVMがFD3/4を予約していたりして要件に合わなかったのでバイナリにした
*5:mruby本体がmirbを作る手法を真似した