Macのrubyのreadlineに不具合がある!?(2)

Macのrubyのreadlineに不具合がある!?の続き。

ruby_1_8ブランチext/readline/readline.cを読む。

上から順番に読んでいく。

TOLOWER

#define TOLOWER(c) (isupper(c) ? tolower(c) : c)

これは、なんのへんてつもない大文字に変換するマクロですが、file.cにも定義してあるようです。

#if !defined(TOLOWER)
#define TOLOWER(c) (ISUPPER(c) ? tolower(c) : (c))
#endif

file.cより

また、TOLOWERでは、isupperではなく、ISUPPERを使った方が良さそうです。

#define ISUPPER(c) (ISASCII(c) && isupper((int)(unsigned char)(c)))

ruby.hより

せっかくなので、リファクタリングしてみます。ISUPPERがruby.hに記述してあることから、ruby.hに追記してみます。

Index: ext/readline/readline.c
===================================================================
--- ext/readline/readline.c	(revision 18037)
+++ ext/readline/readline.c	(working copy)
@@ -26,8 +26,6 @@
 
 static VALUE mReadline;
 
-#define TOLOWER(c) (isupper(c) ? tolower(c) : c)
-
 #define COMPLETION_PROC "completion_proc"
 #define COMPLETION_CASE_FOLD "completion_case_fold"
 static ID completion_proc, completion_case_fold;
Index: ruby.h
===================================================================
--- ruby.h	(revision 18037)
+++ ruby.h	(working copy)
@@ -66,6 +66,9 @@
 #define ISDIGIT(c) (ISASCII(c) && isdigit((int)(unsigned char)(c)))
 #define ISXDIGIT(c) (ISASCII(c) && isxdigit((int)(unsigned char)(c)))
 #endif
+#if !defined(TOLOWER)
+#define TOLOWER(c) (ISUPPER(c) ? tolower(c) : (c))
+#endif
 
 #if defined(HAVE_ALLOCA_H)
 #include <alloca.h>
Index: file.c
===================================================================
--- file.c	(revision 18037)
+++ file.c	(working copy)
@@ -2515,10 +2515,6 @@
     buflen = RSTRING(result)->len,\
     pend = p + buflen)
 
-#if !defined(TOLOWER)
-#define TOLOWER(c) (ISUPPER(c) ? tolower(c) : (c))
-#endif
-
 static int is_absolute_path _((const char*));
 
 static VALUE

脱線しました。続けます。

readline_event

static int readline_event(void);
...
    rl_event_hook = readline_event;

Readline ライブラリのグローバル変数 rl_instream に対して select(2) により入力を待つ。
Readline ライブラリのグローバル変数 rl_event_hook にこの関数を設定する。

readline_readline

static VALUE
readline_readline(argc, argv, self)
...
    rb_define_module_function(mReadline, "readline",
			      readline_readline, -1);
...

ユーザからのキー入力を待つ。ユーザが文字列を入力し終えると、入力した文字列を返す。
次の2つの省略可能な引数を取る。

  • prompt: カーソルの前に表示する文字列を指定する。
  • add_hist: 真ならば入力した文字列をヒストリに記録する。

何も入力していない状態で EOF を入力するなどにより、ユーザからの入力がない場合は nil を返す。

標準入力が tty でなく、すでにクローズされている(isatty(2) の errno が EBADF である)場合、例外 IOError が発生する。

以下が読んでも理解できなかったので調査した。GNU Readline の readline 関数を呼び出しているだけだった。rb_protect が受け取れるように readline 関数を「(VALUE(*)_((VALUE)))」という型にキャストしているのが読めなかった。なお、rb_protect は C の関数を呼び出す関数でした。

    buff = (char*)rb_protect((VALUE(*)_((VALUE)))readline, (VALUE)prompt,
                              &status);

本メソッドのソースに[ruby-dev:29116]とある。

Subject: [ruby-dev:29116] Re: irb completion
From: Tanaka Akira <akr fsij.org>
Date: Mon, 24 Jul 2006 01:18:03 +0900
References: 29064 29070 29114
In-reply-to: 29114

In article <87hd18vgah.fsf / fsij.org>,
  Tanaka Akira <akr / fsij.org> writes:

> (gdb) p (void *)rl_deprep_term_function
> $2 = (void *) 0x0

It is libedit problem.

It is possible on other than darwin.

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/29116より
darwin の libedit だけ、 rl_deprep_term_function に NULL(0x0) が設定されている、と読める。 darwin だけというのがやばい。このような報告があったため、前田さんは MacRuby は、 Readline が...というような感想を持つようになられたのだろうか。

readline_s_set_completion_proc

static VALUE
readline_s_set_completion_proc(self, proc)
...
    rb_define_singleton_method(mReadline, "completion_proc=",
			       readline_s_set_completion_proc, 1);

Proc オブジェクトということではなく、 call メソッドを持つオブジェクトであればなんでも良いようだ。

readline_attempted_completion_function

static char **
readline_attempted_completion_function(text, start, end)

入力した文字列に対する候補を取得する。このとき completion_proc= で設定した候補を取得する処理を実行する(@completion_proc.call(入力した文字列))。
戻り値は(result)はchar *の配列へのポインタで、例えば次のようになる。

completion_proc.call は、["/dev", "/doc", "/disk"]を返すとする。

Readline.completion_proc = proc {|s| ["/dev", "/doc", "/disk"]}

「/」を入力後、tab キーを押す。すると、 以下のようになる。

  • result[0]: "/d"
  • result[1]: "/dev"
  • result[2]: "/doc"
  • result[3]: "/disk"
  • result[4]: NULL

最初の要素は候補の共通部分。上記の場合は、 "/d" までは全ての候補で共通している。
2番目から最後の1つ前までは候補。最後は NULL。

readline_s_set_completion_append_character

static VALUE
readline_s_set_completion_append_character(self, str)
...
    rb_define_singleton_method(mReadline, "completion_append_character=",
			       readline_s_set_completion_append_character, 1);

nil を指定すると、 Readline ライブラリの変数 rl_completion_append_character に '\0' を代入する。
文字列を指定すると、最初の1文字だけを Readline ライブラリの変数 rl_completion_append_character に代入する。
最終的な補完結果の末尾に指定した文字を追加する。

rl_completion_append_character はなんのためにあるのだろうか。きっと目的があるはず。あとで調べる。

まとめ

ext/readline/readline.cの全てに目を通した。
とてもシンプルだと思う。Readline ライブラリの readline 関数を呼び出しているだけという感じ。
こんなにシンプルなのに、 Mac の editline で動作が想定と違うということは、 Mac の editline がまずい気がする。まぁ、なんか回避策があるのだろう。

次は以下に挙げる GNU Readline の API を読む。

  • readline
  • add_history
  • remove_history
  • history_base
  • history_length
  • history_get
  • replace_history_entry
  • rl_completion_append_character
  • rl_basic_word_break_characters
  • rl_completer_word_break_characters
  • rl_basic_quote_characters
  • rl_completer_quote_characters
  • rl_filename_quote_characters