Ctrl-D の話

Mosh の REPL が Ctrl-D で抜けられないとご指摘いただいていた件。
そもそも Ctrl-D って何だっけ?と立ち止まり調べましたが当たり前すぎて(?)記事にすらなってないので書いておきます。

ユーザーから見た Ctrl-D

入力終了を対話型のプログラムにしらせることに使う。

例えば irb から抜けるとき。

dekisugi% irb
irb(main):001:0> puts "Hello"
Hello
=> nil
irb(main):002:0> # Ctrl-D で irb から抜ける

その対話型プログラムが持つ exit や quit などの終了コマンドを入力するよりも楽ですね。
人によっては Ctrl-C を使う場合もあるかもしれません。(そのプログラムが SIGINT をどう扱っているかに依存するので、Ctrl-c で終了しない場合もよくあります。)


追記 id:aql が書いてくれている例(g:tenmon:id:aql:20080602:1212389549)

% perl
print "hello\n";
print 2**10,"\n";
# ... ここでCtrl-D
hello
1024
%

Ctrl-Dで入力の完了を Perl に教えてあげて、Perl は入力を評価してから終了。

プログラムから見た Ctrl-D

プログラムは Ctrl-D をどうハンドルすれば良いだろうか?
Ctrl-D が入力されるとプログラムの標準入力に EOF が入力されます。
なので EOF かどうかのチェックをすることで Ctrl-D をハンドルすることができます。


ソースを見たわけではないのですが、端末(エミュレータ)がシェルの標準入力に EOF を書き込むのだと思われます。(←自信ない)

ややこしい話

bashzsh 上では Ctrl-D は1文字消去なんですよね。

ところで

この Ctrl-D 文化ってシグナルとかと同じく仕様があるんだろうか。
DOS の場合は Ctrl-z ですよね。

Mosh の REPLの改善例

eof-object? で判定するようにしました。

(define (repl . x)
  (define (rec)
    (display "mosh>")
    (guard (e
            [e
             (print e)
             (rec)])
           (let1 obj (read (current-input-port))
             (if (eof-object? obj)
                 (exit)
                 (print (eval obj '())))))
    (rec))
  (rec))

(repl)

追記

shiro さんからコメントで色々教えていただきました。
とても分かりやすく面白いのでぜひご覧下さい。

と思ったが全文引用します。

shiro 『Ctrl-Dは端末ハンドラが解釈します。シェルのプロンプトで

stty -a

と打ってみてください。現在の端末ハンドラの設定が表示されます。ボーレートなど、現在となってはほとんど意味のない設定も出てきますが。 ”eof=^D” とあればCtrl-Dが入力された時に端末ハンドラはEOFをプロセスに送るということです。

”intr=^C” というのも見えると思います。Ctrl-Cが入力されたときにプロセスにSIGINTを送る、というのも端末ハンドラの役割なんですね。端末につながっているプログラム自身はCtrl-Cを見ることはありません。

Ctrl-Cがintr、Ctrl-Dがeof…というのはあくまでデフォルトの設定で、ユーザ側でいくらでも変えられます。変えるのにもsttyを使います。かつて、シリアルポートやモデムを通じて物理的な端末で接続していた頃は、この設定が端末ごとにちゃんとなってないといろいろ苦労したものです。

端末ハンドラはデフォルトで行バッファを持っていて、ユーザがENTERを押すまではハンドラ内に打たれた文字をためておき、ENTERでプロセスに一行分をまとめて渡します。viなどのエディタや、readlineライブラリのように自前で行編集処理などを行いたい場合は、プロセス側から端末を”rawモード”に変更します。

端末ハンドラの行バッファが有効になっているときに、eraseに指定された文字で一文字消去ができます。かつてはこれのデフォルトが^? (DELETE)だったり^H (BACKSPACE) だったりして、いらいらさせられることが多かったものです。stty erase ^H とかよく打ったなあ。最近の、readlineが効くようなシェルではrawモードで端末ハンドラをバイパスしてreadlineライブラリが直接キー入力を解釈してますから、このへんは悩まなくても両方解釈してくれたりします。』(2008/06/02 16:27)

higepon 『ありがとうございます<(_ _)>

>”eof=^D” とあればCtrl-Dが入力された時に端末ハンドラはEOFをプロセスに送るということです。

ありました。なるほど。

端末ハンドラってのは端末エミュレータとは違うんでしょうか。

以前 Mona のために ^? (DELETE)だったり^H (BACKSPACE) あたりを解釈するライブラリを書いて Mona のシェルに指令を出していたんですが。(そのときは ECMA48とかいう仕様の一部を実装していました。)

自分の勝手な想像では

ユーザーキー入力 ==> 端末エミュレータ ==> シェル(親) ==> 子プロセス

みたいな流れかなと思っていたのですが。』(2008/06/02 16:33)

shiro『「エミュレータ」というからには、かつてはエミュレータではない本物の「端末」---ホストコンピュータとは独立したハードウェア---があったのです。

/dev/ttyxx

ユーザ <===> 端末 <====================> 端末ドライバ <-----------> プロセス(シェルなど)

シリアル回線・モデムなど

プロセスから見える/dev/ttyxxというのは端末ドライバと会話します。端末ドライバは通常それぞれのハードウェアの口(シリアルポートとか。より具体的には特定のシリアルポートをコントロールするチップ。8251などが定番だった)に張り付いています。termiosなどのインタフェースでもって、プロセスから端末ドライバの設定を変更できます。(Gauchegauche.termiosはそのSchemeバインディング)。

なお、シェルは子プロセス起動時に既にオープンしてある/dev/ttyxxへのファイルディスクリプタを渡すので、子プロセスも直接端末ドライバと会話します。シェルは間に介入しません。

ホストコンピュータ自身がウィンドウシステムなどを持って外部の「端末」を必要としなくなった時代でも、プロセス側のこれまでのソフトウェア資産を変更するのは大変なので、プロセスと端末ドライバの関係はそのまま保存されました。かわりに、端末ドライバの話す先が実際のハードウェアではなく端末エミュレータという別のソフトウェアになったわけです。端末エミュレータはウィンドウシステムなどから入力を供給されて、端末のふりをして端末ドライバと会話します。

ユーザ <===> ウィンドウシステムなど <------> 端末エミュレータ <-----> 端末ドライバ <-------> プロセス

ウィンドウシステムを使っている場合、端末エミュレータと端末ドライバの間の通信はカーネルが処理します(仮想端末ptyという機能を使います)。

なお、端末専用装置が廃れたけれどまだモデムでホストに接続するのが行われていた過渡期には、ユーザは手元のコンピュータで「端末エミュレータ」を起動して、自分のコンピュータを「端末」のように振る舞わせていました。この場合、端末エミュレータと端末ドライバの間は昔と同じくシリアル回線やモデムということになります。』(2008/06/02 17:06)