Terminal curses

Shugo Maeda 氏(以下、Maeda):最初に自己紹介をしたいと思います。

私は前田修吾と言いまして、Rubyのコミッターなんですが、最近はあまりコミットしてないですね。Naclという会社の取締役で、「Rubyアソシエーション」というRubyの普及と発展のための組織があるんですがその事務局長もしてます。

いくつかプロダクトがりますが最近使っているものだと「Textbringer」というテキストエディタを作っています。この間1.0.0をついにリリースしました。あまり変わってないんですけど、0.3.いくつとかだと使ってくれないなぁって思ってバージョンを上げてみました。

あと、そのTextbringerで動くメールソフトも作っていて、このプレゼンツール自体もTextbringerで動いています。さっきプレゼンした原君が僕と同じ会社なので、内容がWebとTerminalでぜんぜん違って「なんで同じ枠だろう」と思ってたんですけど、最初「同じ会社だからかなぁ」と思ったんですけど、恐らくですけど自作プレゼンツールを使っていて、しかもユーザが1人しかいないツールを作っているというそういう枠なのかなぁと思ってます。

Textbringerのステッカーを作りまして、これ自費で作ったんですけど、まだいっぱい余っているのでほしい人がいたら教えてください。

今日のタイトルが「Terminal curses」というタイトルなんですけど、これはダジャレでして。松田さんにも通じなかったのですこし無粋ですがダジャレの説明をさせてください。

Terminalというのが形容詞の意味もあって、死が近いような状態をTerminalと言うそうなんですが、cursesは呪いですね。つまり、「死に至るような呪いのお話」を今日はしたいと思います。

今日の話題なんですが、Terminalの基本的なお話をして、どうやってRubyでプログラミングするかとか、あとはcursesというCのライブラリとそのバイディングのお話をしたいと思います。

ターゲットプラットフォームですが、主に「GNU/Linux」を対象にお話します。だいたいの話題はUnix-likeや他のプラットフォームでも当てはまると思います。いくつかWindowsのお話もしたいと思います。

Terminalとは何か?

最初に「Terminalとは何か?」なんですけど、これvt100という本物のテキストターミナルです。

もともとは専用の端末があって、スクリーンとキーボードがあって、ホストコンピュータと呼ばれるものにつながっていて、インターフェースだけ提供しているものです。

ただ、僕も触ったことがなくて、普段みなさんが使われているのは「Terminal emulators」と呼ばれるPC上で動いているものだと思います。

なぜTerminalを使うのか。

みなさん普段GUIのインターフェースを使っていると思います。テキストベースで抽象化されたインターフェースなので、操作性もいいですし、Compatibilityというかポータビリティかもしれないですけど、プラットフォーム間の差も比較的少ない……ぜんぜん少なくないのでまた後で話しますけど(笑)。

まぁ、実装するのは簡単ですし、リモートコンピューティングもしやすい。あとは一番重要なのが、Terminal使っていると仕事しているように見えるのでおすすめです。

Terminalのインターフェースとしてユーザインターフェースという側面と、プログラミングをするインターフェースという2つの側面についてお話したいと思います。

ユーザインターフェースは、みなさんご存知の通りスクリーンがあってキーボードがある。アプリケーションインターフェースとしては主に2通りありまして、CUIはコマンドを1行打ち込むと結果が返ってきて、というのを繰り返すようなインターフェースですね。例えばshellだったりとかline editorsみたいなものです。

TUIというは、今画面に出ているテキストエディタもTUIですが、画面を使ってカーソルが自由に動いて、画面全体を使えるようなスクリーンエディタなどです。

プログラミングのインターフェースとしては、主にファイルの入出力に使います。

みなさんはRubyを使っていたらSTDINとSTDOUTとSTDEERって使われていると思うんですけど、このファイルの実態が、例えばLinuxだと最初の1行目みたいにprocの下に特別なファイルがあって、今のプロセスのfdというディレクトリの下にファイルディスクリプタの番号が並んでいて、実態が分かったりします。

このエディタはRubyで書いているのでエディタ上でRubyプログラムが実行できて、一番下のechoエリアに結果が出るんですけど、今のこの端末だと"/dev/pts/3"というファイルが実態ということがわかります。

ただ"dev/pts/file"だと、これ使えないので、fdevnameという関数があるので、それをFiddleとかで呼ぶと同じようなことができます。

デバイスの種類がいくつかあって、1番上の/dev/tty{1..63}というのはLinuxだとバーチャルコンソールと呼ばれるものがありまして、だいたいみなさんLinuxを使われるとグラフィカルログインをされる方が多いと思います。そうじゃない人もいるかも知れませんが(笑)。テキストログインできるようなコンソールというのがあって、「ctrl+Ali+Fnキー」とかで複数のコンソールを仮想的に切り替えられるようになってます。

ttyS{0..}から連番がついていて、シリアルポート……最近のPCだとシリアルポートはあまり付いていないので、ttyUSB0とかを使うことのほうが多いかもしれないですけれども、シリアルコンソールと呼ばれるものは、これを使います。

最後のものが恐らく、みなさんが普通Terminal emulatorsで1番よく使われてるんじゃないかなと思います。

特殊な端末デバイスがありまして、さっきのtty{1..63}というのはバーチャルコンソールなんですが、0番はカレントのバーチャルコンソールを指します。番号が付いていない/dev/ttyというのがコントロールターミナルという制御端末を指します。

プロセス・プロセスグループ・セッションについて

制御端末の説明をするために、ちょっとプロセスとプロセスグループとセッションのお話を少しだけ説明しますけど、UNIXのシステムだとプロセスというのはプロセスグループに属していて、それぞれのプロセスグループというのはセッションに属していて、セッションに制御端末が結びついていったり、場合によってはデーモンとかは制御端末がなかったりします。

これを開くと、そのプロセスに結びついている制御端末が取れますが、Windowsでも似たようなものがあって、「con」という特殊な名前のファイルを開くと、同じようにコンソールが開けるようになっています。

デーモンなどの場合は制御端末を切り離したいことが多いので、その場合は「Process.setsid」というRubyのメソッドを呼んであげると新しいセッションを作るので、最初セッションができたときは制御端末がない状態でオープンしようとするとこういうエラーが発生します。

あるいは、下の例のはちょっと文字列の……閉じてなかった。エディタなので間違っててもすぐ直せるんですけど、I/O Controlで制御端末を捨てることもできます。

逆にアタッチするときですが、Linuxだと他のセッションで使われていない端末デバイスを開くとその時点で制御端末が獲得されて、明示的にI/O Controlで獲得することもできますけれども、LinuxだとI/O Controlの引数で1を使うと特殊な権限を持ったユーザの場合だと、他のセッションですでに使われている制御端末でも奪うことができるという機能があります。

「FreeBSD」の場合は、そういうオプションはなくて、使われていない端末だけ制御端末として割り当てられます。

ptyとは何か

ここでptyの説明をしたいと思います。

疑似端末と呼ばれるものなんですけど、仮想的なデバイスのペアでパイプに似たような感じなんですが、masterとslaveがあってプロセス間でやりとりができるようなものです。

"/dev/ptmx"というのがmaster clone deviceというもので、親プロセス側でこれをオープンするとmasterとslaveの組みが使えるようになるってかたちで、例えばsshとかtelnetみたいなものを実装したりとか、Terminal emulatorsの実装とかで使われます。

最近のWindowsにptyに似た機能も入ったそうで、僕はまだ試してはいないんですけど、「ConPty」というインターフェースができているようです。

図にするとこんな感じで、親プロセスと子プロセスがいて、ptyと、ptyのmasterとslaveをそれぞれ持っていて、入出力をやりとりできるというかたちですね。

さっきのTerminal emulatorsの実装以外でユースケースがあって、例えば、出力が端末かどうかによって挙動が変わるようなプログラムがあるんですけども、これを他のプログラムから呼び出すときに、その呼び出される側のプログラムを書き換えれれば挙動を変えられるんですけど、OSの標準のコマンドとかで呼び出したりするときはそういうことができないので、疑似端末を使って呼び出される側のプログラムを騙してやるということをします。

例えば、CのSTDOUTというのは標準だと端末の場合には1行出力されると即時に画面に文字が表示されるんですが、端末じゃない時はバッファリングされてデッドロックしたりするので、そういうときに使ったりします。

あと最近はRubyだと端末かどうかでバックトレースの向きが変わってなかなか覚えられない人も多いと思いますが、ときどきそういうプログラムがあります。

プログラム側でttyかどうかを見るためには、Cだと「isatty()」という関数がありますし、Rubyだと「tty?」というメソッドがあります。もう1つ「/dev/tty」という、例えば、「リダイレクトされている時でも制御端末と直接やりとりしたい」みたいなときにオープンするプログラムがあるんですが、そういうプログラムを騙すときにも使えます。

これはRubyのptyライブラリのサンプルに載っているようなコードなんですけど、この例だとfactorというコマンドを呼んで素因数分解をするUnixの標準的なプログラムなんですけど、42って書いてやってコマンドの出力を受け取るという……。

ptyを使ってないと、getsのところでバッファリングされたままでfactorのプロセス上のメモリーのバッファに乗っているだけでフラッシュしてくれないので、ここでデッドロックします。これ僕試してみたら自分の環境で動かなくて、「これはRubyのバグに違いない」と思ったんですけど、FreeBSDだとちゃんと動いて、あと「PTY.spawn」を使ってもちゃんと動くので、「なんかおかしいなぁ」と思って「GNU coreutils」のソースコードを取ってきてコード読んでたら、最近のcoreutilsだとSTDIOを使ってなくて自分でバッファリングしてるみたいなんです。

ttyかどうかでline_bufferedというフラグを変えているんですけど、よく見るとこれ「STDIN」って書いてあって。

普通CのSTDIOライブラリですと、STDOUTが端末かどうかで挙動を変えているんですけど、「これが書き間違いじゃないかなぁ」と思って、こういうパッチを送りました。

一応マージされたんですけど、マージするときに修正されてマージされまして、STDINもSTDOUTも両方見るようにして、どっちかが端末だったらline_bufferedにするというのが修正になってます。

なんでそうされたかが説明されてたんですけど、factorの結果をパイプで他のプログラムに渡したりすると、この場合だとSTDINだけ端末になるんですが、この時も結果がすぐ見えるようにしたいということでこういう修正がされたみたいです。

Control characters

ここからもう少し端末の話をしたいと思います。

Control charactersと言われる文字がいくつかあります。端末を制御するための特殊な文字なんですけれど、「キャレット記号」というものがあってアルファベットにキャレットを付けるとコントロールキャラクターを表すような記号が一般的に使われます。

Rubyのリテラルとしてはキャレット記号ではなくて、"\C-h"と書くと「⌃H」という意味になるんですが、実はEmacsと互換性があって非常に便利な機能なのでみなさん使ってください。

サンプルとしては、「puts "I love Perl〜"」と書いてバックスペース4つでRubyって書くと……。ちょっとテキストエディタ上でputsしちゃってるんで若干表示が崩れますけど、Perl文字が消えて「Ruby」って出ます。

あとは、エスケープシーケンスと呼ばれるものがあって、例えば「"\e[1;1H"」って書くと1行目1列目にカーソルが移動して、下のように書くと文字が赤色で出ます。

あと「Sixel」という特殊なエスケープシーケンスもあって、DECの、エミュレータとかじゃない実際の端末でもサポートしているものがあったそうなんですが、Bitmap graphicsのフォーマットが使えるというもので、6pixelsが1個のASCII文字に割り当てられるような形になってて、端末上で画像が表示できます。さっきステッカーの画像とか出してたのもこれを使っています。

例えばこんなふうに書くと、これはすごくわかりにくいと思いますが(笑)、ここに黄色くでているものがそうなんですが、僕のノートPCが4Kでして、それをミラーリングしているのでそのせいもあってかなり小さくなっています(笑)。まあこれだけ書いてもこれしか出せないのて、ちょっと効率が悪いかなと思います。

このようにいろいろなエスケープシーケンスがありますが、端末の種類によって使えるものが違っていて、1個1個プログラムで自分で判別するのは大変なので、termcapやterminfoといったデータベースが用意されています。

これを見ると、今使っている端末がどんな機能をサポートしているのかがわかるようになっています。termcapというのがもともと作られたもので、システムファーム系だけ改良されたのがterminfoです。Rubyだとruby-terminfoというakrさんが作られたライブラリを使うことができます。

入力について

ここからは入力に関する話をしたいと思います。canonical modeとnon-canonical modeという物がありまして、普通はシェル上でコマンドを実行するときは一行一行入力することが多いので、基本的にcanonical modeというモードが使われています。基本的な行編集ができるようになっています。例えばコマンドを打ったときに1文字間違えたりするとそれを直すためにBackSpaceで消してEnterを押すと、そこではじめて修正された文字列がプログラムに渡る形になります。

non-canonical modeは、先ほどのcanonical modeと違って直接1文字1文字処理できるのですが、内部ではタイマーを使ったバッファがありまして、本当に1文字1文字取ってくると効率が悪いので、ある程度バッファのサイズを指定したりとか、あとは待つ時間を指定して、一定時間内で最初の何文字、最小限の文字数だけ取りたいという指定ができるようになっています。

この仕組みが、だいたいシステムコールのwriteとかreadとかを呼ぶと、line disciplineという機構を伝って各端末のデバイスごとにデータが渡ります。line disciplineというところで、先ほど説明した行編集の機能や入力でCRが来たらLFに変換したりだとか、出力の時はLFを出力するとCRLFになるといった制御が行われます。

line disciplineにはいくつが種類があって、TTYというのが端末で使われるものです。最近はあまり使っていないですが、SLIPというCRライン上でIPを実装するような機能もあります。

あとは入力の際のコントロールキャラクタがいくつか特殊な動作を行えるようになっています。

例えばcarriage returnやline feedもありますし、^Cを押すとその文字がプログラムに来る代わりに、SIGINTというシグナルが発生したり、いろいろそういった制御が行われます。

Rubyではそういったモードの変更や端末の処理に特化した機能を実装するために、io/consoleという標準ライブラリがあります。これがIOに対していろいろなメソッドを追加してれます。

例えばnoechoというメソッドを呼ぶと、このブロックの中では文字の入力に対してecho backが行われなくなるので、例えばパスワードの入力をするときなどにecho backをすると嬉しくないので、こんなふうにするとecho backせずに一行に入力を読むことができます。

rawというメソッドは、先ほど説明したのはno-canonical modeというモードになったり、no echo backになったり、^Cで割り込みされる機能が無効化されます。

IO.consoleというのは、制御端末に対応するデバイスファイルを開いて返してくれるというメソッドがありまして、これを使うとLinuxでもWindowsでも同じように制御端末を取得できます。