Macのrubyのreadlineに不具合がある!?(5)
Macのrubyのreadlineに不具合がある!?(4)の続き。
Ruby の Readline ライブラリをデバッグする。
問題は以下。
http://redmine.ruby-lang.org/issues/show/212
Ruby の Readline ライブラリが GNU Readline を使用するようにしてコンパイルされた場合、以下のように振る舞う。
require "readline" puts Readline::VERSION Readline::HISTORY.push("1", "2", "3") Readline::HISTORY.each { |i| puts i } # 1、2、3を順番に出力する。
しかし、Ruby の Readline ライブラリが Editline Library を使用するようにしてコンパイルされた場合、以下のように振る舞う。
require "readline" puts Readline::VERSION Readline::HISTORY.push("1", "2", "3") Readline::HISTORY.each { |i| puts i } # 1は出力せずに、2、3を順番に出力する。
Readline::HISTORY.each は、ヒストリの最初から最後まで histroy_get で取り出しブロックに渡す、というのを繰り返す。
static VALUE hist_each(self) VALUE self; { HIST_ENTRY *entry; int i; rb_secure(4); for (i = 0; i < history_length; i++) { entry = history_get(history_base + i); if (entry == NULL) break; rb_yield(rb_tainted_str_new2(entry->line)); } return self; }
これが期待通りに動作していない。libedit のヒストリには魔物がすんでいるようだ。
C でコードを書いて、GNU Readline と libedit の動作を確認してみよう。
#include <stdio.h> #ifdef HAVE_EDITLINE_READLINE_H #include <editline/readline.h> #else #include <readline/readline.h> #include <readline/history.h> #endif /* HAVE_EDITLINE_READLINE_H */ int main(int argc, char **argv) { HIST_ENTRY *entry; int i; using_history(); /* puts Readline::VERSION */ printf("%s\n", rl_library_version); /* Readline::HISTORY.push("1", "2", "3") */ add_history("1"); add_history("2"); add_history("3"); /* Readline::HISTORY.each { |i| puts i } */ for (i = 0; i < history_length; i++) { entry = history_get(history_base + i); if (entry == NULL) { break; } printf("%s\n", entry->line); } return 0; }
$ gcc no212_on-macosx.c -o no212_on-macosx.gnu_readline -L/opt/local/lib -lreadline $ gcc no212_on-macosx.c -DHAVE_EDITLINE_HEADER_H -o no212_on-macosx.editline -L/Developer/SDKs/MacOSX10.5.sdk/usr/lib -lreadline
実行。
$ ./no212_on-macosx.gnu_readline 5.2 1 2 3 $ ./no212_on-macosx.editline EditLine wrapper 2 3
想定通りの結果になった。これから回避策を探そう。
調査の結果、 history_base の扱いが GNU Readline と libedit では違うことが分かった。
GNU Readline の history_base から 1 を引いたものが libedit の history_base のようだ。
この想定が正しいならば、以下のパッチで問題が回避できる。
Index: ext/readline/readline.c =================================================================== --- ext/readline/readline.c (revision 18037) +++ ext/readline/readline.c (working copy) @@ -511,6 +511,12 @@ #endif /* HAVE_RL_FILENAME_QUOTE_CHARACTERS */ } +#ifdef HAVE_EDITLINE_READLINE_H +#define HISTORY_BASE (history_base - 1) +#else +#define HISTORY_BASE (history_base) +#endif + static VALUE hist_to_s(self) VALUE self; @@ -531,7 +537,7 @@ if (i < 0) { i += history_length; } - entry = history_get(history_base + i); + entry = history_get(HISTORY_BASE + i); if (entry == NULL) { rb_raise(rb_eIndexError, "invalid index"); } @@ -649,7 +655,7 @@ rb_secure(4); for (i = 0; i < history_length; i++) { - entry = history_get(history_base + i); + entry = history_get(HISTORY_BASE + i); if (entry == NULL) break; rb_yield(rb_tainted_str_new2(entry->line));
パッチを当てた後、動作を確認する。
$ cd /path/to/ruby_1_8 $ ./configure --prefix=${HOME}/local --program-suffix=18trunk --enable-libedit $ ~/local/bin/ruby18trunk -rreadline -e 'puts Readline::VERSION; Readline::HISTORY.push("1", "2", "3"); Readline::HISTORY.each { |i| puts i }' EditLine wrapper 1 2 3
よっしゃ。これでうまくいった。
とりあえず、ITSやruby-dev MLに現状やパッチを報告する。
http://redmine.ruby-lang.org/issues/show/212
今後は、configure の --enable-libedit を指定しなくてもいいようにしたい。
コンパイル時に GNU Readline と libedit を判定したいが方法が分からない。
とりあえず、実行時に判定することにしよう。