Bug #212 [ruby-core:17479]

Issues with Readline in Mac OS X

Added by Federico Builes 153 days ago. Updated 80 days ago.

Status :Closed Start :07/01/2008
Priority :Normal Due date :
Assigned to :- % Done :

0%

Category :-
Target version :-

Description

There are currently some issue with the Readline wrappers for Editline in Mac OS 10.5, e.g.:

    require 'readline'
    puts Readline::VERSION
    Readline::HISTORY.push("1", "2", "3")
    Readline::HISTORY.each { |i| puts i }
    # => "EditLine wrapper"
    # => 2
    # => 3

Any idea if this is an issue with the wrapper or if it's a bug with the underlying library? 
Compiling ruby with GNU Readline works fine.

no212.patch - history_base - 1 with libedit (953 Bytes) Kouji Takao, 07/13/2008 09:09 PM

no212_ext-readline-readline.c.patch - for 1.8.6. (4.1 KB) Kouji Takao, 08/14/2008 01:38 AM

no212_ext-readline-readline.c.patch - for 1.8 branch. (11.4 KB) Kouji Takao, 08/14/2008 01:38 AM

History

07/02/2008 09:13 AM - Roger Pack

May have something to do with the terminal you're using

07/02/2008 10:11 AM - Federico Builes

Roger: This is part of the Rubyspecs test suite but that's why we push the items into it, do you think there's anything else we should check?

07/02/2008 03:33 PM - Luis Lavena

As Federico suggested, has nothing to do with the terminal emulation or console being used, but with the shared object being involved.

If you do a full GNU Readline linkage instead, the error goes away. I think is related to the macros it tries during the build process of the readline extension and the have_readline_header("editline/readline.h") and have_library("edit", "readline").

07/13/2008 09:09 PM - Kouji Takao

= summary

This problem was investigated. 

As for GNU Readline's history_base and libedit's it, the treatment is different. 
GNU Readline's history_base = libedit's history_base - 1.

= example

== GNU Readline

HIST_ENTRY *entry;

add_history("1");
add_history("2");
add_history("3");

entry = history_get(history_base);
printf("%s\n", entry->line); /* => 1 */

entry = history_get(history_base + 1);
printf("%s\n", entry->line); /* => 2 */

entry = history_get(history_base + 2);
printf("%s\n", entry->line); /* => 2 */


== Editline Library

HIST_ENTRY *entry;

add_history("1");
add_history("2");
add_history("3");

entry = history_get(history_base - 1);
printf("%s\n", entry->line); /* => 1 */

entry = history_get(history_base);
printf("%s\n", entry->line); /* => 2 */

entry = history_get(history_base + 1);
printf("%s\n", entry->line); /* => 3 */

= try

I created the patch for this problem.
Try the patch.
1. Mac OSX 10.5(Leopard).
2. apply patch to ruby 1.8.
3. run configure with --enable-libedit.
4. run make.
5. run make install.
6. run below command.
$ ruby -rreadline -e 'puts Readline::VERSION; Readline::HISTORY.push("1", "2", "3"); Readline::HISTORY.each { |i| puts i }'

= environment

My environment.
* Mac OSX: 10.5.3
* ruby: ruby_1_8 branch(revision:18037, 2008-07-12 16:02:47 +0900 (Sat, 12 Jul 2008))
* libedit: $NetBSD: readline.h,v 1.18 2006/08/21 12:45:30 christos Exp $

07/18/2008 11:38 AM - Keita Yamaguchi

山口と申します。

2008/7/17 Takao Kouji <kouji@takao7.net>:
> どうも、ヒストリのインデックスの開始が、
> * GNU Readline は 1。(history_base と同じ)
> * libedit は 0。
> という違いがあるようです。
> このため、remove_historyなども同様に修正する必要がありました。
> これは以下のコードで確認しました。

このコードを Ubuntu 8.04 の libedit で実行してみたのですが、
GNU ReadLine の結果と同一になりました。

% ./a.out
history_base: 1
added 1, 2, 3 to history
history_base: 1
history_length: 3
history_get(history_base): 1
history_get(0): NULL
history_get(1): 1
history_get(2): 2
history_get(3): 3

% ldd a.out
	linux-gate.so.1 =>  (0xb7f91000)
	libedit.so.2 => /usr/lib/libedit.so.2 (0xb7f53000)
	libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e04000)
	libncurses.so.5 => /lib/libncurses.so.5 (0xb7dd3000)
	/lib/ld-linux.so.2 (0xb7f92000)
	libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb7dcf000)

それで不思議に思って少し調べてみました。
Ubuntu 8.04 の libedit は少し古くて libedit-2.9.cvs.20050518 といった感じです。
そこで http://www.thrysoee.dk/editline/ からダウンロードした
libedit-20080712-2.11.tar.gz を試したところ、
高尾さんの結果と同一になりました。

history_base: 1
added 1, 2, 3 to history
history_base: 1
history_length: 3
history_get(history_base): 2
history_get(0): 1
history_get(1): 2
history_get(2): 3
history_get(3): NULL

間違っていたら申し訳ないのですが、多分 2007/08/12 に入った
http://cvsweb.netbsd.org/bsdweb.cgi/src/lib/libedit/readline.c.diff?r1=1.71&r2=1.72&sortby=date
の変更の影響ではないかと思われます。
特に

-	if (history(h, &ev, H_NEXT_EVENT, num))
+	if (history(h, &ev, H_NEXT_EVENT, num + 1))

のあたりが疑わしいと考えています。
これを元に戻してコンパイルし直したところ、
GNU Readline と同じ挙動になりました。

history_base: 1
added 1, 2, 3 to history
history_base: 1
history_length: 3
history_get(history_base): 1
history_get(0): NULL
history_get(1): 1
history_get(2): 2
history_get(3): 3

もしこれが正しいのだとすると、
libedit のバージョンによって挙動が変わることになるのではないかと思います。
ひょっとして Mac OSX のバージョンによっては古い libedit が入っていたりして、
高尾さんのパッチによって影響を受けてしまう可能性はありませんでしょうか。

私は Mac OSX 環境を持っていませんので調べることが出来ないのですが、
一応 10.4 あたりでも試してみた方が良いのではないかと思います。

---
山口慶太

07/18/2008 11:39 AM - Kouji Takao

高尾宏治です。

On 2008/07/17, at 5:38, Keita Yamaguchi wrote:
> 2008/7/17 Takao Kouji <kouji@takao7.net>:
>> どうも、ヒストリのインデックスの開始が、
>> * GNU Readline は 1。(history_base と同じ)
>> * libedit は 0。
>> という違いがあるようです。
>> このため、remove_historyなども同様に修正する必要がありました。
>> これは以下のコードで確認しました。
>
> このコードを Ubuntu 8.04 の libedit で実行してみたのですが、
> GNU ReadLine の結果と同一になりました。
(snip)
> libedit のバージョンによって挙動が変わることになるのではないかと思います。
> ひょっとして Mac OSX のバージョンによっては古い libedit が入っていたりして、
> 高尾さんのパッチによって影響を受けてしまう可能性はありませんでしょうか。

情報ありがとうございます。
libedit についてもう少し調査が必要ですね。

ヒストリに関する単体テストを書いてみて、
いろいろな環境でテストしてみたいと思います。

しかしながら、 libedit の各バージョンを試すのは面倒ですね。
Readline の初期化時に VERSION ではなく、
ヒストリの操作を試した結果、 offset を計算したほうがいいかもしれませんね。
# しかし、そうまでして libedit に対応する必要があるのだろうか。。。

07/18/2008 11:39 AM - Kouji Takao

高尾宏治です。

On 2008/07/17, at 9:21, M.Suzuki wrote:
>> 私は Mac OSX 環境を持っていませんので調べることが出来ないのですが、
>> 一応 10.4 あたりでも試してみた方が良いのではないかと思います。
>
> 試してみましたが、Mac OSX 10.4では(history_baseに関しては)
> 問題ないようです。
>
> msuzuki$ uname -a
> Darwin PowerBookG4.local 8.11.0 Darwin Kernel Version 8.11.0: Wed Oct
> 10 18:26:00 PDT 2007; root:xnu-792.24.17~1/RELEASE_PPC Power Macintosh
> powerpc
> msuzuki$ cc -o libedittest libedittest.c -ledit
> msuzuki$ ./libedittest
> history_base: 1 added 1, 2, 3 to history history_base: 1
> history_length: 3
> history_get(history_base): 1
> history_get(0): NULL
> history_get(1): NULL
> history_get(2): NULL
> history_get(3): NULL

history_get(0) から (3) までが全て NULL ですね。
これは奇妙ですね。
なんか Mac OSX 10.4 では Readline のヒストリの扱いが駄目な気がします。
これは回避できないかもしれません。
まいったな。

07/18/2008 11:41 AM - Anonymous

M.Suzukiと申します。

> 私は Mac OSX 環境を持っていませんので調べることが出来ないのですが、
> 一応 10.4 あたりでも試してみた方が良いのではないかと思います。

試してみましたが、Mac OSX 10.4では(history_baseに関しては)
問題ないようです。

msuzuki$ uname -a
Darwin PowerBookG4.local 8.11.0 Darwin Kernel Version 8.11.0: Wed Oct
10 18:26:00 PDT 2007; root:xnu-792.24.17~1/RELEASE_PPC Power Macintosh
powerpc
msuzuki$ cc -o libedittest libedittest.c -ledit
msuzuki$ ./libedittest
history_base: 1 added 1, 2, 3 to history history_base: 1
history_length: 3
history_get(history_base): 1
history_get(0): NULL
history_get(1): NULL
history_get(2): NULL
history_get(3): NULL

ちなみに、MacPortsのreadlineを使うと他と同じ結果になりました。

msuzuki$ cc -o libedittest libedittest.c -L/opt/local/lib -lreadline
msuzuki$ ./libedittest
history_base: 1
added 1, 2, 3 to history
history_base: 1
history_length: 3
history_get(history_base): 1
history_get(0): NULL
history_get(1): 1
history_get(2): 2
history_get(3): 3
--
M.Suzuki :: Idea -> Coffe -> (Code, Bug)
--

07/18/2008 11:42 AM - Kouji Takao

高尾宏治です。

# Subjectを変えます。

On 2008/07/16, at 15:33, Nobuyoshi Nakada wrote:
> At Wed, 16 Jul 2008 11:45:06 +0900,
> Shugo Maeda wrote in [ruby-dev:35532]:
>> Ruby側でとりあえず対応するという点については異論はないのですが、
>> バグだとしたらバージョンによって挙動が変わったら嫌だなと思いました。
>
> 初期化のときにチェックするなら、バージョンが変わっても気にしなく
> てすむと思います。

初期化の時にチェックするのも良さそうですね。
ただし、Readlineの拡張ライブラリの初期化時に、
毎回 libedit とリンクしているかどうかを確認するのは、
どうかということがあると思います。
これについては、Init_readline()で以下のような処理をすると思うのですが、
対したコストではないため、気にしなくても良いのかもしれません。

  if (strncmp(rl_library_version, "EditLine wrapper", 
              strlen("EditLine wrapper")) == 0) {
      /* libedit の場合の処理 */
  }
  else {
      /* libedit の場合の処理 */
  }

>>>> 高尾君のパッチと大分趣が異なるような気がするのですが、どういう意図でしょうか。
>>>
>>> history_baseの初期値を保存しておいて、そこからの差分で見るという
>>> ことです。
>>
>> 自分がlibeditの挙動を理解していないだけかもしれないのですが、それで上手く
>> 動くのでしょうか。
>> 高尾君のパッチではたんにhistory_baseをhistory_base - 1にしているだけだった
>> ので、libeditではhistory_baseの値が1ずれているとかそういう話なのかなと思った
>> のですが。
>
> そのズレの値を初期状態でとれないかと思ったのですが、初期値はずれ
> ているわけではないとしたら的外れかもしれません。後は、
> remove_history()があるならですが、[ruby-dev:35519]のテストコード
> を実行してみるとかしかないですね。
>
> ついでにいえば、remove_history()やreplace_history_entry()も同じ
> くindexをとるわけですが、こちらは問題ないんでしょうか。

問題がありました。
どうも、ヒストリのインデックスの開始が、
* GNU Readline は 1。(history_base と同じ)
* libedit は 0。
という違いがあるようです。
このため、remove_historyなども同様に修正する必要がありました。
これは以下のコードで確認しました。
----- ここから -----
#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();

  printf("history_base: %d\n", history_base);

  add_history("1");
  add_history("2");
  add_history("3");

  printf("added 1, 2, 3 to history\n");
  printf("history_base: %d\n", history_base);
  printf("history_length: %d\n", history_length);

  entry = history_get(history_base);
  printf("history_get(history_base): %s\n", entry->line);
  
  for (i = 0; i <= history_length; i++) {
    entry = history_get(i);
    printf("history_get(%d): %s\n", i, entry ? entry->line : "NULL");
  }
  
  return 0;
}
----- ここまで -----

以下、GNU Readline、Mac OSX の libedit、
libedit の 20080712-2.11 で実行した結果です。
----- ここから -----
---- no212_on-macosx.gnu_readline ----
history_base: 1
added 1, 2, 3 to history
history_base: 1
history_length: 3
history_get(history_base): 1
history_get(0): NULL
history_get(1): 1
history_get(2): 2
history_get(3): 3
---- no212_on-macosx.editline ----
history_base: 1
added 1, 2, 3 to history
history_base: 1
history_length: 3
history_get(history_base): 2
history_get(0): 1
history_get(1): 2
history_get(2): 3
history_get(3): NULL
---- no212_on-macosx.editline_20080712-2.11 ----
history_base: 1
added 1, 2, 3 to history
history_base: 1
history_length: 3
history_get(history_base): 2
history_get(0): 1
history_get(1): 2
history_get(2): 3
history_get(3): NULL
----- ここまで -----

最後にこれまでの話などを考慮したパッチを作成しました。
実行時に libedit であれば index のオフセットを 0 に、
そうでない(GNU Readline であれば) オフセットを history_base に
設定するようにしています。

Mac OSX 10.5.4(昨日10.5.3からアップデートしました)の環境で、
動作を確認しています。

$ ~/local/bin/ruby19trunk -rreadline -ve 'puts Readline::VERSION; Readline::HISTORY.push("1", "2", "3"); Readline::HISTORY.each { |s| puts s }; p Readline::HISTORY[1]; Readline::HISTORY[1] = "A" ; p Readline::HISTORY[1]; Readline::HISTORY.delete_at(0); p Readline::HISTORY[0]; p Readline::HISTORY.length; p Readline::HISTORY.pop; p Readline::HISTORY.pop; p Readline::HISTORY.pop; '
ruby 1.9.0 (2008-07-16 revision 18071) [i686-darwin9.4.0]
EditLine wrapper
1
2
3
"2"
"A"
"A"
2
"3"
"A"
nil

----- ここから -----
Index: ext/readline/readline.c
===================================================================
--- ext/readline/readline.c	(revision 18084)
+++ ext/readline/readline.c	(working copy)
@@ -43,6 +43,8 @@
 # define rl_completion_matches completion_matches
 #endif
 
+static int history_offset = 0;
+
 static char **readline_attempted_completion_function(const char *text,
                                                      int start, int end);
 
@@ -516,7 +518,7 @@
     if (i < 0) {
         i += history_length;
     }
-    entry = history_get(history_base + i);
+    entry = history_get(i + history_offset);
     if (entry == NULL) {
 	rb_raise(rb_eIndexError, "invalid index");
     }
@@ -536,7 +538,7 @@
     if (i < 0) {
         i += history_length;
     }
-    entry = replace_history_entry(i, RSTRING_PTR(str), NULL);
+    entry = replace_history_entry(i + history_offset, RSTRING_PTR(str), NULL);
     if (entry == NULL) {
 	rb_raise(rb_eIndexError, "invalid index");
     }
@@ -578,7 +580,7 @@
     VALUE val;
 
     rb_secure(4);
-    entry = remove_history(index);
+    entry = remove_history(index + history_offset);
     if (entry) {
         val = rb_tainted_str_new2(entry->line);
         free(entry->line);
@@ -624,7 +626,7 @@
 
     rb_secure(4);
     for (i = 0; i < history_length; i++) {
-        entry = history_get(history_base + i);
+        entry = history_get(i + history_offset);
         if (entry == NULL)
             break;
 	rb_yield(rb_tainted_str_new2(entry->line));
@@ -795,6 +797,10 @@
     rb_define_const(mReadline, "USERNAME_COMPLETION_PROC", ucomp);
 #if defined HAVE_RL_LIBRARY_VERSION
     rb_define_const(mReadline, "VERSION", rb_str_new2(rl_library_version));
+    if (strncmp(rl_library_version, "EditLine wrapper", 
+		strlen("EditLine wrapper")) != 0) {
+	history_offset = history_base;
+    }
 #else
     rb_define_const(mReadline, "VERSION", rb_str_new2("2.0 or prior version"));
 #endif

07/19/2008 09:29 AM - Kouji Takao

高尾宏治です。

On 2008/07/17, at 9:16, Takao Kouji wrote:
> On 2008/07/17, at 5:38, Keita Yamaguchi wrote:
>> 2008/7/17 Takao Kouji <kouji@takao7.net>:
>>> どうも、ヒストリのインデックスの開始が、
>>> * GNU Readline は 1。(history_base と同じ)
>>> * libedit は 0。
>>> という違いがあるようです。
>>> このため、remove_historyなども同様に修正する必要がありました。
>>> これは以下のコードで確認しました。
>>
>> このコードを Ubuntu 8.04 の libedit で実行してみたのですが、
>> GNU ReadLine の結果と同一になりました。
> (snip)
>> libedit のバージョンによって挙動が変わることになるのではないかと思います。
>> ひょっとして Mac OSX のバージョンによっては古い libedit が入っていたりして、
>> 高尾さんのパッチによって影響を受けてしまう可能性はありませんでしょうか。
>
> 情報ありがとうございます。
> libedit についてもう少し調査が必要ですね。
>
> ヒストリに関する単体テストを書いてみて、
> いろいろな環境でテストしてみたいと思います。

ということで、Readline::HISTORYに対する単体テストを作成しました。
Readline::HISTORYの全てのメソッドのテストを記述しています。
本メールの下部に記述します。

このテストを GNU Readline と Mac OSX 10.5 の libedit で
実行した結果を以下に示します。

----- ここから -----
GNU Readline
~/work/ruby/ruby_1_9 $ ./ruby19trunk -Iext/readline/gnu-readline test/readline/test_readline_history.rb
Loaded suite test/readline/test_readline_history
Started
.................
Finished in 0.004555 seconds.

17 tests, 159 assertions, 0 failures, 0 errors


libedit
~/work/ruby/ruby_1_9 $ ./ruby19trunk -Iext/readline/editline test/readline/test_readline_history.rb
Loaded suite test/readline/test_readline_history
Started
..F..FEF..EEFFF..
Finished in 0.017272 seconds.

1) Failure:
test_each(Readline::TestHistory)
 [test/readline/test_readline_history.rb:144:in `block in test_each'
  test/readline/test_readline_history.rb:142:in `each'
  test/readline/test_readline_history.rb:142:in `test_each']:
<"1:a"> expected but was
<"2:b">.

2) Failure:
test_get(Readline::TestHistory)
 [test/readline/test_readline_history.rb:22:in `block in test_get'
  test/readline/test_readline_history.rb:21:in `each'
  test/readline/test_readline_history.rb:21:in `each_with_index'
  test/readline/test_readline_history.rb:21:in `test_get']:
<"1:a"> expected but was
<"2:b">.

3) Error:
test_get__negative(Readline::TestHistory):
IndexError: invalid index
 test/readline/test_readline_history.rb:29:in `[]'
 test/readline/test_readline_history.rb:29:in `block in test_get__negative'
 test/readline/test_readline_history.rb:28:in `each'
 test/readline/test_readline_history.rb:28:in `test_get__negative'

4) Failure:
test_get__out_of_range(Readline::TestHistory)
 [test/readline/test_readline_history.rb:37:in `block in test_get__out_of_range'
  test/readline/test_readline_history.rb:36:in `each'
  test/readline/test_readline_history.rb:36:in `test_get__out_of_range']:
i=<-6>.
<IndexError> exception expected but none was thrown.

5) Error:
test_push(Readline::TestHistory):
IndexError: invalid index
 test/readline/test_readline_history.rb:85:in `[]'
 test/readline/test_readline_history.rb:85:in `block in test_push'
 test/readline/test_readline_history.rb:83:in `times'
 test/readline/test_readline_history.rb:83:in `test_push'

6) Error:
test_push__operator(Readline::TestHistory):
IndexError: invalid index
 test/readline/test_readline_history.rb:93:in `[]'
 test/readline/test_readline_history.rb:93:in `block in test_push__operator'
 test/readline/test_readline_history.rb:91:in `times'
 test/readline/test_readline_history.rb:91:in `test_push__operator'

7) Failure:
test_push__plural(Readline::TestHistory)
 [test/readline/test_readline_history.rb:101:in `block in test_push__plural'
  test/readline/test_readline_history.rb:100:in `each'
  test/readline/test_readline_history.rb:100:in `test_push__plural']:
<"0"> expected but was
<"1">.

8) Failure:
test_set(Readline::TestHistory)
 [test/readline/test_readline_history.rb:56:in `block in test_set'
  test/readline/test_readline_history.rb:53:in `times'
  test/readline/test_readline_history.rb:53:in `test_set']:
<"set: 0"> expected but was
<"2:b">.

9) Failure:
test_set__out_of_range(Readline::TestHistory)
 [test/readline/test_readline_history.rb:68:in `block in test_set__out_of_range'
  test/readline/test_readline_history.rb:67:in `each'
  test/readline/test_readline_history.rb:67:in `test_set__out_of_range']:
index=<-6>.
<IndexError> exception expected but none was thrown.

17 tests, 98 assertions, 6 failures, 3 errors
----- ここまで -----

これからテストが通るようにext/readline/readline.cを修正します。

以下、パッチ。
----- ここから -----
Index: test/readline/test_readline_history.rb
===================================================================
--- test/readline/test_readline_history.rb	(revision 0)
+++ test/readline/test_readline_history.rb	(revision 0)
@@ -0,0 +1,238 @@
+begin
+  require "readline"
+rescue LoadError
+else
+  require "test/unit"
+end
+
+class Readline::TestHistory < Test::Unit::TestCase
+  include Readline
+
+  def setup
+    clear_history
+  end
+  
+  def test_to_s
+    assert_equal("HISTORY", HISTORY.to_s)
+  end
+
+  def test_get
+    lines = push_history(5)
+    lines.each_with_index do |s, i|
+      assert_equal(s, HISTORY[i])
+    end
+  end
+
+  def test_get__negative
+    lines = push_history(5)
+    (1..5).each do |i|
+      assert_equal(lines[-i], HISTORY[-i])
+    end
+  end
+
+  def test_get__out_of_range
+    lines = push_history(5)
+    invalid_indexes = [5, 6, 100, -6, -7, -100]
+    invalid_indexes.each do |i|
+      assert_raise(IndexError, "i=<#{i}>") do
+        HISTORY[i]
+      end
+    end
+    
+    invalid_indexes = [100_000_000_000_000_000_000,
+                       -100_000_000_000_000_000_000]
+    invalid_indexes.each do |i|
+      assert_raise(RangeError, "i=<#{i}>") do
+        HISTORY[i]
+      end
+    end
+  end
+
+  def test_set
+    lines = push_history(5)
+    5.times do |i|
+      expected = "set: #{i}"
+      HISTORY[i] = expected
+      assert_equal(expected, HISTORY[i])
+    end
+  end
+
+  def test_set__out_of_range
+    assert_raise(IndexError, "index=<0>") do
+      HISTORY[0] = "set: 0"
+    end
+    
+    lines = push_history(5)
+    invalid_indexes = [5, 6, 100, -6, -7, -100]
+    invalid_indexes.each do |i|
+      assert_raise(IndexError, "index=<#{i}>") do
+        HISTORY[i] = "set: #{i}"
+      end
+    end
+    
+    invalid_indexes = [100_000_000_000_000_000_000,
+                       -100_000_000_000_000_000_000]
+    invalid_indexes.each do |i|
+      assert_raise(RangeError, "index=<#{i}>") do
+        HISTORY[i] = "set: #{i}"
+      end
+    end
+  end
+
+  def test_push
+    5.times do |i|
+      assert_equal(HISTORY, HISTORY.push(i.to_s))
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(5, HISTORY.length)
+  end
+
+  def test_push__operator
+    5.times do |i|
+      assert_equal(HISTORY, HISTORY << i.to_s)
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(5, HISTORY.length)
+  end
+
+  def test_push__plural
+    assert_equal(HISTORY, HISTORY.push("0", "1", "2", "3", "4"))
+    (0..4).each do |i|
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(5, HISTORY.length)
+
+    assert_equal(HISTORY, HISTORY.push("5", "6", "7", "8", "9"))
+    (5..9).each do |i|
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(10, HISTORY.length)
+  end
+
+  def test_pop
+    assert_equal(nil, HISTORY.pop)
+    
+    lines = push_history(5)
+    (1..5).each do |i|
+      assert_equal(lines[-i], HISTORY.pop)
+      assert_equal(lines.length - i, HISTORY.length)
+    end
+    
+    assert_equal(nil, HISTORY.pop)
+  end
+
+  def test_shift
+    assert_equal(nil, HISTORY.shift)
+    
+    lines = push_history(5)
+    (0..4).each do |i|
+      assert_equal(lines[i], HISTORY.shift)
+      assert_equal(lines.length - (i + 1), HISTORY.length)
+    end
+    
+    assert_equal(nil, HISTORY.shift)
+  end
+
+  def test_each
+    HISTORY.each do |s|
+      assert(false) # not reachable
+    end
+    lines = push_history(5)
+    i = 0
+    HISTORY.each do |s|
+      assert_equal(HISTORY[i], s)
+      assert_equal(lines[i], s)
+      i += 1
+    end
+  end
+
+  def test_each__enumerator
+    e = HISTORY.each
+    assert_instance_of(Enumerable::Enumerator, e)
+  end
+
+  def test_length
+    assert_equal(0, HISTORY.length)
+    push_history(1)
+    assert_equal(1, HISTORY.length)
+    push_history(4)
+    assert_equal(5, HISTORY.length)
+    clear_history
+    assert_equal(0, HISTORY.length)
+  end
+
+  def test_empty_p
+    2.times do
+      assert(HISTORY.empty?)
+      HISTORY.push("s")
+      assert_equal(false, HISTORY.empty?)
+      HISTORY.pop
+      assert(HISTORY.empty?)
+    end
+  end
+
+  def test_delete_at
+    lines = push_history(5)
+    (0..4).each do |i|
+      assert_equal(lines[i], HISTORY.delete_at(0))
+    end
+    assert(HISTORY.empty?)
+
+    lines = push_history(5)
+    (1..5).each do |i|
+      assert_equal(lines[lines.length - i], HISTORY.delete_at(-1))
+    end
+    assert(HISTORY.empty?)
+
+    lines = push_history(5)
+    assert_equal(lines[0], HISTORY.delete_at(0))
+    assert_equal(lines[4], HISTORY.delete_at(3))
+    assert_equal(lines[1], HISTORY.delete_at(0))
+    assert_equal(lines[3], HISTORY.delete_at(1))
+    assert_equal(lines[2], HISTORY.delete_at(0))
+    assert(HISTORY.empty?)
+  end
+
+  def test_delete_at__out_of_range
+    assert_raise(IndexError, "index=<0>") do
+      HISTORY.delete_at(0)
+    end
+    
+    lines = push_history(5)
+    invalid_indexes = [5, 6, 100, -6, -7, -100]
+    invalid_indexes.each do |i|
+      assert_raise(IndexError, "index=<#{i}>") do
+        HISTORY.delete_at(i)
+      end
+    end
+    
+    invalid_indexes = [100_000_000_000_000_000_000,
+                       -100_000_000_000_000_000_000]
+    invalid_indexes.each do |i|
+      assert_raise(RangeError, "index=<#{i}>") do
+        HISTORY.delete_at(i)
+      end
+    end
+  end
+
+  private
+
+  def push_history(num)
+    lines = []
+    num.times do |i|
+      s = "a"
+      i.times do
+        s = s.succ
+      end
+      lines.push("#{i + 1}:#{s}")
+    end
+    HISTORY.push(*lines)
+    return lines
+  end
+
+  def clear_history
+    while HISTORY.pop
+    end
+    assert(HISTORY.empty?)
+  end
+end if defined?(::Readline) && defined?(::Readline::HISTORY)

07/19/2008 11:10 PM - Kouji Takao

高尾宏治です。

On 2008/07/19, at 9:24, Takao Kouji wrote:
> ということで、Readline::HISTORYに対する単体テストを作成しました。
> Readline::HISTORYの全てのメソッドのテストを記述しています。

今回の問題に対応するためのパッチを作成しました。
# テストがあるっていいですね。

パッチの概要を次に述べます。
* GNU Readline を使用した場合の挙動は一切変えません。(変えないように実装したつもりです。)
* パッチの適用前のReadlineモジュールには、
  EditLine(libedit) を使用する場合のみ次の問題があります。
  * libedit のバージョン (2.9より上?) によっては、
    history_get 関数で指定するインデックスは 0 から始まる。
    history_base からではない。
    これが原因で、 Readline::HISTORY ではユーザからの最初の入力を取得できない。
  * history_get 関数で -1 を指定すると 0 番目の history を取得する。
    GNU Readline では NULL である。
    これが原因で、 Readline::HISTORY[-100] では IndexError 例外が発生しない。
  * replace_history_entry 関数で -1 を指定すると 0 番目の history を操作する。
    GNU Readline では操作できない。
    これが原因で、 Readline::HISTORY[-100] = "abc" などにより
    0 番目の history を操作してしまう。
* 上記を回避するため、次のことを行う。
  * Init_readline 関数で libedit かどうかを判定する。
  * libedit であれば、 history の操作時の最初のインデックスの番号が
    history_base か 0 かを調査する。
  * 上記の情報を hist_get (Readline::HISTORY[] -> string | nil) で使用する。
  * また、 hist_get や hist_set (Readline::HISTORY[index] = string) で
    Ruby で index が負の値かどうかチェックする。

なお、パッチの概要は前田さんとも情報共有できていると思うのですが、
実際のパッチの内容まではまだ見ていただいてない状態です。
お手数ですが、確認をお願いします。 > 前田さん

私の手元の環境である Mac OSXのlibedit、
Mac OSX で MacPorts で入れた GNU Readline 5.2 でしか
動作を確認できていません。
そこで、 [ruby-dev:35553] で報告があった Ubuntu 8.04 の libedit や
Mac OSX 10.4 の libedit で試していただける方を募集します。

試す方法を以下に書きます。
1. ruby の trunk を用意する。
2. このメールのパッチと [ruby-dev:35588] のパッチを当てる。
   $ cd /path/to/ruby/trunk
   $ patch -p0 < /path/to/patch
3. configure に --with-libedit を付けて実行する。
   $ ./configure --with-libedit
4. make
5. 単体テストの実行。
   $ ./ruby -I.ext/<arch dir> -rreadline \
   test/readline/test_readline_history.rb

上記のテストがパスできれば OK です。
パスできなかった場合は教えてください。がんばります。

以下、パッチです。
----- ここから -----
Index: readline.c
===================================================================
--- readline.c	(revision 18084)
+++ readline.c	(working copy)
@@ -29,6 +29,8 @@
 
 static VALUE mReadline;
 
+#define EDIT_LINE_LIBRARY_VERSION "EditLine wrapper"
+
 #define COMPLETION_PROC "completion_proc"
 #define COMPLETION_CASE_FOLD "completion_case_fold"
 static ID completion_proc, completion_case_fold;
@@ -43,6 +45,8 @@
 # define rl_completion_matches completion_matches
 #endif
 
+static int (*history_get_offset_func)(int);
+
 static char **readline_attempted_completion_function(const char *text,
                                                      int start, int end);
 
@@ -505,10 +509,22 @@
     return rb_str_new2("HISTORY");
 }
 
+static int
+history_get_offset_history_base(int offset)
+{
+    return history_base + offset;
+}
+
+static int
+history_get_offset_0(int offset)
+{
+    return offset;
+}
+
 static VALUE
 hist_get(VALUE self, VALUE index)
 {
-    HIST_ENTRY *entry;
+    HIST_ENTRY *entry = NULL;
     int i;
 
     rb_secure(4);
@@ -516,7 +532,9 @@
     if (i < 0) {
         i += history_length;
     }
-    entry = history_get(history_base + i);
+    if (i >= 0) {
+	entry = history_get(history_get_offset_func(i));
+    }
     if (entry == NULL) {
 	rb_raise(rb_eIndexError, "invalid index");
     }
@@ -527,7 +545,7 @@
 hist_set(VALUE self, VALUE index, VALUE str)
 {
 #ifdef HAVE_REPLACE_HISTORY_ENTRY
-    HIST_ENTRY *entry;
+    HIST_ENTRY *entry = NULL;
     int i;
 
     rb_secure(4);
@@ -536,7 +554,9 @@
     if (i < 0) {
         i += history_length;
     }
-    entry = replace_history_entry(i, RSTRING_PTR(str), NULL);
+    if (i >= 0) {
+	entry = replace_history_entry(i, RSTRING_PTR(str), NULL);
+    }
     if (entry == NULL) {
 	rb_raise(rb_eIndexError, "invalid index");
     }
@@ -581,7 +601,7 @@
     entry = remove_history(index);
     if (entry) {
         val = rb_tainted_str_new2(entry->line);
-        free(entry->line);
+        free((void *) entry->line);
         free(entry);
         return val;
     }
@@ -624,7 +644,7 @@
 
     rb_secure(4);
     for (i = 0; i < history_length; i++) {
-        entry = history_get(history_base + i);
+        entry = history_get(history_get_offset_func(i));
         if (entry == NULL)
             break;
 	rb_yield(rb_tainted_str_new2(entry->line));
@@ -793,8 +813,17 @@
     rb_define_singleton_method(ucomp, "call",
 			       username_completion_proc_call, 1);
     rb_define_const(mReadline, "USERNAME_COMPLETION_PROC", ucomp);
+    history_get_offset_func = history_get_offset_history_base;
 #if defined HAVE_RL_LIBRARY_VERSION
     rb_define_const(mReadline, "VERSION", rb_str_new2(rl_library_version));
+    if (strncmp(rl_library_version, EDIT_LINE_LIBRARY_VERSION, 
+		strlen(EDIT_LINE_LIBRARY_VERSION)) == 0) {
+	add_history("1");
+	if (history_get(history_get_offset_func(0)) == NULL) {
+	    history_get_offset_func = history_get_offset_0;
+	}
+	clear_history();
+    }
 #else
     rb_define_const(mReadline, "VERSION", rb_str_new2("2.0 or prior version"));
 #endif

07/19/2008 11:23 PM - Kouji Takao

高尾宏治です。

間違えがありました。

On 2008/07/19, at 23:05, Takao Kouji wrote:
> 試す方法を以下に書きます。
> 1. ruby の trunk を用意する。
> 2. このメールのパッチと [ruby-dev:35588] のパッチを当てる。
>   $ cd /path/to/ruby/trunk
>   $ patch -p0 < /path/to/patch
> 3. configure に --with-libedit を付けて実行する。
>   $ ./configure --with-libedit

--with-libeditではなく、--enable-libeditでした。
失礼しました。

> 4. make
> 5. 単体テストの実行。
>   $ ./ruby -I.ext/<arch dir> -rreadline \
>   test/readline/test_readline_history.rb
>
> 上記のテストがパスできれば OK です。
> パスできなかった場合は教えてください。がんばります。

07/20/2008 03:09 PM - Keita Yamaguchi

山口と申します。

2008/7/19 Takao Kouji <kouji@takao7.net>:
> 私の手元の環境である Mac OSXのlibedit、
> Mac OSX で MacPorts で入れた GNU Readline 5.2 でしか
> 動作を確認できていません。
> そこで、 [ruby-dev:35553] で報告があった Ubuntu 8.04 の libedit や
> Mac OSX 10.4 の libedit で試していただける方を募集します。

Ubuntu 8.04 において

- GNU Readline 5.2
- libedit(2.9.cvs.20050518)

に対するテストを実施致しましたので状況を報告致します。



1. GNU Readline 5.2

keita% ./ruby -I .ext/i686-linux/ -rreadline
test/readline/test_readline_history.rb
Loaded suite test/readline/test_readline_history
Started
.................
Finished in 0.047366958 seconds.

17 tests, 159 assertions, 0 failures, 0 errors

以上、Ubuntu 8.04 の GNU Readline 5.2 では一切問題ありません。



2. libedit(2.9.cvs.20050518)

Loaded suite test/readline/test_readline_history
Started
EEEEEEEEEEEEEEEEE
Finished in 0.001803326 seconds.

  1) Error:
test_delete_at(Readline::TestHistory):
NotImplementedError: pop() function is unimplemented on this machine
    test/readline/test_readline_history.rb:234:in `pop'
    test/readline/test_readline_history.rb:234:in `clear_history'
    test/readline/test_readline_history.rb:12:in `setup'

以下17まで同様なので略しますが、以上のように全てのテストに失敗します。

このエラーの原因は、
setup から HISTORY.pop が呼ばれる際に最終的に remove_history が呼ばれるのですが、
remove_history は Ubuntu 8.04 の 2.9.cvs.20050518 版に存在しないからです
(libedit に remove_history が追加されたのは 2005/07/14 になります)。

参考までに --enable-libedit した場合に生成される extconf.h の内容は次のようになります。

keita% cat ext/readline/extconf.h
#ifndef EXTCONF_H
#define EXTCONF_H
#define HAVE_EDITLINE_READLINE_H 1
#define HAVE_RL_COMPLETION_MATCHES 1
#define HAVE_RL_DEPREP_TERM_FUNCTION 1
#define HAVE_RL_COMPLETION_APPEND_CHARACTER 1
#define HAVE_RL_BASIC_WORD_BREAK_CHARACTERS 1
#define HAVE_RL_COMPLETER_WORD_BREAK_CHARACTERS 1
#define HAVE_RL_COMPLETER_QUOTE_CHARACTERS 1
#define HAVE_RL_ATTEMPTED_COMPLETION_OVER 1
#define HAVE_RL_LIBRARY_VERSION 1
#endif

このように HAVE_REMOVE_HISTORY が存在しないため、
rb_remove_history において rb_notimplement が呼ばれます。
このためにテストの setup 時において1-17の全てが失敗します。

remove_history に限らず、
2.9.cvs.20050518 では他にもいくつかヒストリ系の関数が欠けているようです。
このため Ubuntu 8.04 のように古い libedit を使用している環境では、
ひょっとするとヒストリ系の操作はそもそもうまくいかないのかも知れません。



以上です。
現実的には Ubuntu における libedit の状況を考慮する
必要性は一切ないと思いますが、一応報告致します。

---
山口慶太

07/22/2008 12:05 PM - Kouji Takao

高尾宏治です。

On 2008/07/20, at 15:04, Keita Yamaguchi wrote:
>
> Ubuntu 8.04 において
>
> - GNU Readline 5.2
> - libedit(2.9.cvs.20050518)
>
> に対するテストを実施致しましたので状況を報告致します。

作業いただき、ありがとうございます。

> 2. libedit(2.9.cvs.20050518)
>
> Loaded suite test/readline/test_readline_history
> Started
> EEEEEEEEEEEEEEEEE
> Finished in 0.001803326 seconds.
>
> 1) Error:
> test_delete_at(Readline::TestHistory):
> NotImplementedError: pop() function is unimplemented on this machine
> test/readline/test_readline_history.rb:234:in `pop'
> test/readline/test_readline_history.rb:234:in `clear_history'
> test/readline/test_readline_history.rb:12:in `setup'
>
> 以下17まで同様なので略しますが、以上のように全てのテストに失敗します。
>
> このエラーの原因は、
> setup から HISTORY.pop が呼ばれる際に最終的に remove_history が呼ばれるのですが、
> remove_history は Ubuntu 8.04 の 2.9.cvs.20050518 版に存在しないからです
> (libedit に remove_history が追加されたのは 2005/07/14 になります)。

remove_history がない環境があるのですね。
というか、Readlineモジュールはないのを想定した実装になっているので、
対応しないといけなかったですね。

そのような環境でのテスト内容を検討する必要がありますね。
選択肢としては以下のものを考えました。
1. そのような環境では入力履歴のテストを行わない。
2. remove_history のテストはしない。
clear_history が使えるかどうかを確認し、これを使うことで最低限のテストを行う。

いまのところ、後者で対応したいと考えています。
まずは、clear_history があるかどうかの確認してみます。
あるようならば、 Readline::HISTORY.clear メソッドを追加したいと考えています。
# この提案が通るかどうかは分かりませんが...

また、その他のReadline::HISTORYのメソッドも、
例外NotImplementErrorの発生を考慮したテストに修正します。

しかし、私はなかなか面倒な問題に手を出してしまったのですね。

07/30/2008 03:20 PM - Kouji Takao

高尾宏治です。

On 2008/07/22, at 12:55, M.Suzuki wrote:
> 試してみました。
(snip)
> やっと目的(?)の結果が得られました。
>
> msuzuki$ ./ruby -I.ext/powerpc-darwin8.11.0 -rreadline test/readline/test_readline_history.rb
> Loaded suite test/readline/test_readline_history
> Started
> EEEEEEEEEEEEEEEEE
> Finished in 0.014429 seconds.
>
> 1) Error:
> test_delete_at(Readline::TestHistory):
> NotImplementedError: pop() function is unimplemented on this machine
>  test/readline/test_readline_history.rb:234:in `pop'
>  test/readline/test_readline_history.rb:234:in `clear_history'
>  test/readline/test_readline_history.rb:12:in `setup'
> ...
>
> ext/readline/extconf.h の内容も念のため貼っておきます。
>
> #ifndef EXTCONF_H
> #define EXTCONF_H
> #define HAVE_READLINE_READLINE_H 1
> #define HAVE_RL_DEPREP_TERM_FUNCTION 1
> #define HAVE_RL_COMPLETION_APPEND_CHARACTER 1
> #define HAVE_RL_BASIC_WORD_BREAK_CHARACTERS 1
> #define HAVE_RL_COMPLETER_WORD_BREAK_CHARACTERS 1
> #define HAVE_RL_COMPLETER_QUOTE_CHARACTERS 1
> #define HAVE_RL_LIBRARY_VERSION 1
> #endif
>
> 定義関数は更に少ないです。かなり古いバージョンのようです。

同僚のはからいで、Mac OSX 10.4のアカウントをもらいました。
私の手元でも動作を確認してみます。

ただ、Readlineモジュールの入力履歴を扱うメソッドは、
例外NotImplementedErrorが発生するかもしれないので、
それに対応したテストを記述する必要がありました。
現在、この作業中です。

この作業が終わったら、パッチを本MLに報告する予定です。
Mac OSX 10.4の環境でもテストしてみます。

それと、Mac OSX 10.4 の ruby は、
Readlineモジュールを無効にしているのですね。
$ ruby -v -rreadline -e 'Readline'
ruby 1.8.2 (2004-12-25) [universal-darwin8.0]
ruby: No such file to load -- readline (LoadError)

07/30/2008 03:25 PM - Kouji Takao

高尾宏治です。

On 2008/07/23, at 23:14, Takao Kouji wrote:
> ただ、Readlineモジュールの入力履歴を扱うメソッドは、
> 例外NotImplementedErrorが発生するかもしれないので、
> それに対応したテストを記述する必要がありました。
> 現在、この作業中です。
>
> この作業が終わったら、パッチを本MLに報告する予定です。

テストを修正しました。
本メールの下部にパッチを記述しています。
これまでのパッチをいったんrevertしてから試してください。

今回のパッチには、
$SAFE = 4のときの例外SecurityErrorが発生することを確認するためのテストを追加しています。
あと、入力履歴をクリアするReadline::HISTORY.clearメソッドを追加しています。

> Mac OSX 10.4の環境でもテストしてみます。

まだ、Mac OSX 10.4でのテストはまで作業していません。
あとで作業する予定です。(が、作業していただけると助かります。)
しかしながら、[ruby-dev:35557]の以下の記述から推測すると、
テストに失敗することが想定されます。
> msuzuki$ uname -a
> Darwin PowerBookG4.local 8.11.0 Darwin Kernel Version 8.11.0: Wed Oct
> 10 18:26:00 PDT 2007; root:xnu-792.24.17~1/RELEASE_PPC Power Macintosh
> powerpc
> msuzuki$ cc -o libedittest libedittest.c -ledit
> msuzuki$ ./libedittest
> history_base: 1 added 1, 2, 3 to history history_base: 1
> history_length: 3
> history_get(history_base): 1
> history_get(0): NULL
> history_get(1): NULL
> history_get(2): NULL
> history_get(3): NULL

Mac OSX 10.4の標準添付のrubyが
readlineモジュールをコンパイルしていないことから、
10.4環境でののReadline::HISTORYは無効にしても良いように思います。
無効といってもHISTORYを定義しないとか、
例外NotImplementedErrorが発生とか、いろいろ方法はあると思いますが、
具体的になにがいいかは検討できていません。

以上です。

----- ここから -----
Index: ext/readline/readline.c
===================================================================
--- ext/readline/readline.c	(revision 18194)
+++ ext/readline/readline.c	(working copy)
@@ -29,6 +29,8 @@

static VALUE mReadline;

+#define EDIT_LINE_LIBRARY_VERSION "EditLine wrapper"
+
#define COMPLETION_PROC "completion_proc"
#define COMPLETION_CASE_FOLD "completion_case_fold"
static ID completion_proc, completion_case_fold;
@@ -43,6 +45,8 @@
# define rl_completion_matches completion_matches
#endif

+static int (*history_get_offset_func)(int);
+
static char **readline_attempted_completion_function(const char *text,
                                                     int start, int end);

@@ -505,10 +509,22 @@
    return rb_str_new2("HISTORY");
}

+static int
+history_get_offset_history_base(int offset)
+{
+    return history_base + offset;
+}
+
+static int
+history_get_offset_0(int offset)
+{
+    return offset;
+}
+
static VALUE
hist_get(VALUE self, VALUE index)
{
-    HIST_ENTRY *entry;
+    HIST_ENTRY *entry = NULL;
    int i;

    rb_secure(4);
@@ -516,7 +532,9 @@
    if (i < 0) {
        i += history_length;
    }
-    entry = history_get(history_base + i);
+    if (i >= 0) {
+	entry = history_get(history_get_offset_func(i));
+    }
    if (entry == NULL) {
	rb_raise(rb_eIndexError, "invalid index");
    }
@@ -527,7 +545,7 @@
hist_set(VALUE self, VALUE index, VALUE str)
{
#ifdef HAVE_REPLACE_HISTORY_ENTRY
-    HIST_ENTRY *entry;
+    HIST_ENTRY *entry = NULL;
    int i;

    rb_secure(4);
@@ -536,7 +554,9 @@
    if (i < 0) {
        i += history_length;
    }
-    entry = replace_history_entry(i, RSTRING_PTR(str), NULL);
+    if (i >= 0) {
+	entry = replace_history_entry(i, RSTRING_PTR(str), NULL);
+    }
    if (entry == NULL) {
	rb_raise(rb_eIndexError, "invalid index");
    }
@@ -581,7 +601,7 @@
    entry = remove_history(index);
    if (entry) {
        val = rb_tainted_str_new2(entry->line);
-        free(entry->line);
+        free((void *) entry->line);
        free(entry);
        return val;
    }
@@ -624,7 +644,7 @@

    rb_secure(4);
    for (i = 0; i < history_length; i++) {
-        entry = history_get(history_base + i);
+        entry = history_get(history_get_offset_func(i));
        if (entry == NULL)
            break;
	rb_yield(rb_tainted_str_new2(entry->line));
@@ -662,6 +682,19 @@
}

static VALUE
+hist_clear(VALUE self)
+{
+#ifdef HAVE_CLEAR_HISTORY
+    rb_secure(4);
+    clear_history();
+    return self;
+#else
+    rb_notimplement();
+    return Qnil; /* not reached */
+#endif
+}
+
+static VALUE
filename_completion_proc_call(VALUE self, VALUE str)
{
    VALUE result;
@@ -782,6 +815,7 @@
    rb_define_singleton_method(history,"size", hist_length, 0);
    rb_define_singleton_method(history,"empty?", hist_empty_p, 0);
    rb_define_singleton_method(history,"delete_at", hist_delete_at, 1);
+    rb_define_singleton_method(history,"clear", hist_clear, 0);
    rb_define_const(mReadline, "HISTORY", history);

    fcomp = rb_obj_alloc(rb_cObject);
@@ -793,8 +827,19 @@
    rb_define_singleton_method(ucomp, "call",
			       username_completion_proc_call, 1);
    rb_define_const(mReadline, "USERNAME_COMPLETION_PROC", ucomp);
+    history_get_offset_func = history_get_offset_history_base;
#if defined HAVE_RL_LIBRARY_VERSION
    rb_define_const(mReadline, "VERSION", rb_str_new2(rl_library_version));
+#if defined HAVE_CLEAR_HISTORY
+    if (strncmp(rl_library_version, EDIT_LINE_LIBRARY_VERSION, 
+		strlen(EDIT_LINE_LIBRARY_VERSION)) == 0) {
+	add_history("1");
+	if (history_get(history_get_offset_func(0)) == NULL) {
+	    history_get_offset_func = history_get_offset_0;
+	}
+	clear_history();
+    }
+#endif
#else
    rb_define_const(mReadline, "VERSION", rb_str_new2("2.0 or prior version"));
#endif
Index: ext/readline/extconf.rb
===================================================================
--- ext/readline/extconf.rb	(revision 18194)
+++ ext/readline/extconf.rb	(working copy)
@@ -66,4 +66,5 @@
have_readline_func("rl_emacs_editing_mode")
have_readline_func("replace_history_entry")
have_readline_func("remove_history")
+have_readline_func("clear_history")
create_makefile("readline")
Index: test/readline/test_readline_history.rb
===================================================================
--- test/readline/test_readline_history.rb	(revision 0)
+++ test/readline/test_readline_history.rb	(revision 0)
@@ -0,0 +1,313 @@
+begin
+  require "readline"
+=begin
+  class << Readline::HISTORY
+    def []=(index, str)
+      raise NotImplementedError
+    end
+
+    def pop
+      raise NotImplementedError
+    end
+
+    def shift
+      raise NotImplementedError
+    end
+
+    def delete_at(index)
+      raise NotImplementedError
+    end
+  end
+=end
+
+=begin
+  class << Readline::HISTORY
+    def clear
+      raise NotImplementedError
+    end
+  end
+=end
+rescue LoadError
+else
+  require "test/unit"
+end
+
+class Readline::TestHistory < Test::Unit::TestCase
+  include Readline
+
+  def setup
+    HISTORY.clear
+  end
+
+  def test_safe_level_4
+    method_args =
+      [
+       ["[]", [0]],
+       ["[]=", [0, "s"]],
+       ["\<\<", ["s"]],
+       ["push", ["s"]],
+       ["pop", []],
+       ["shift", []],
+       ["length", []],
+       ["delete_at", [0]],
+       ["clear", []],
+      ]
+    method_args.each do |method_name, args|
+      assert_raises(SecurityError,
+                    "method=<#{method_name}>") do
+        Thread.start {
+          $SAFE = 4
+          HISTORY.send(method_name.to_sym, *args)
+          assert(true)
+        }.join
+      end
+    end
+
+    assert_raises(SecurityError, NotImplementedError,
+                  "method=<each>") do
+      Thread.start {
+        $SAFE = 4
+        HISTORY.each { |s|
+          assert(true)
+        }
+      }.join
+    end
+  end
+  
+  def test_to_s
+    assert_equal("HISTORY", HISTORY.to_s)
+  end
+
+  def test_get
+    lines = push_history(5)
+    lines.each_with_index do |s, i|
+      assert_equal(s, HISTORY[i])
+    end
+  end
+
+  def test_get__negative
+    lines = push_history(5)
+    (1..5).each do |i|
+      assert_equal(lines[-i], HISTORY[-i])
+    end
+  end
+
+  def test_get__out_of_range
+    lines = push_history(5)
+    invalid_indexes = [5, 6, 100, -6, -7, -100]
+    invalid_indexes.each do |i|
+      assert_raise(IndexError, "i=<#{i}>") do
+        HISTORY[i]
+      end
+    end
+    
+    invalid_indexes = [100_000_000_000_000_000_000,
+                       -100_000_000_000_000_000_000]
+    invalid_indexes.each do |i|
+      assert_raise(RangeError, "i=<#{i}>") do
+        HISTORY[i]
+      end
+    end
+  end
+
+  def test_set
+    begin
+      lines = push_history(5)
+      5.times do |i|
+        expected = "set: #{i}"
+        HISTORY[i] = expected
+        assert_equal(expected, HISTORY[i])
+      end
+    rescue NotImplementedError
+    end
+  end
+
+  def test_set__out_of_range
+    assert_raises(IndexError, NotImplementedError, "index=<0>") do
+      HISTORY[0] = "set: 0"
+    end
+    
+    lines = push_history(5)
+    invalid_indexes = [5, 6, 100, -6, -7, -100]
+    invalid_indexes.each do |i|
+      assert_raises(IndexError, NotImplementedError, "index=<#{i}>") do
+        HISTORY[i] = "set: #{i}"
+      end
+    end
+    
+    invalid_indexes = [100_000_000_000_000_000_000,
+                       -100_000_000_000_000_000_000]
+    invalid_indexes.each do |i|
+      assert_raise(RangeError, NotImplementedError, "index=<#{i}>") do
+        HISTORY[i] = "set: #{i}"
+      end
+    end
+  end
+
+  def test_push
+    5.times do |i|
+      assert_equal(HISTORY, HISTORY.push(i.to_s))
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(5, HISTORY.length)
+  end
+
+  def test_push__operator
+    5.times do |i|
+      assert_equal(HISTORY, HISTORY << i.to_s)
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(5, HISTORY.length)
+  end
+
+  def test_push__plural
+    assert_equal(HISTORY, HISTORY.push("0", "1", "2", "3", "4"))
+    (0..4).each do |i|
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(5, HISTORY.length)
+
+    assert_equal(HISTORY, HISTORY.push("5", "6", "7", "8", "9"))
+    (5..9).each do |i|
+      assert_equal(i.to_s, HISTORY[i])
+    end
+    assert_equal(10, HISTORY.length)
+  end
+
+  def test_pop
+    begin
+      assert_equal(nil, HISTORY.pop)
+      
+      lines = push_history(5)
+      (1..5).each do |i|
+        assert_equal(lines[-i], HISTORY.pop)
+        assert_equal(lines.length - i, HISTORY.length)
+      end
+      
+      assert_equal(nil, HISTORY.pop)
+    rescue NotImplementedError
+    end
+  end
+
+  def test_shift
+    begin
+      assert_equal(nil, HISTORY.shift)
+      
+      lines = push_history(5)
+      (0..4).each do |i|
+        assert_equal(lines[i], HISTORY.shift)
+        assert_equal(lines.length - (i + 1), HISTORY.length)
+      end
+    
+      assert_equal(nil, HISTORY.shift)
+    rescue NotImplementedError
+    end
+  end
+
+  def test_each
+    HISTORY.each do |s|
+      assert(false) # not reachable
+    end
+    lines = push_history(5)
+    i = 0
+    HISTORY.each do |s|
+      assert_equal(HISTORY[i], s)
+      assert_equal(lines[i], s)
+      i += 1
+    end
+  end
+
+  def test_each__enumerator
+    e = HISTORY.each
+    assert_instance_of(Enumerable::Enumerator, e)
+  end
+
+  def test_length
+    assert_equal(0, HISTORY.length)
+    push_history(1)
+    assert_equal(1, HISTORY.length)
+    push_history(4)
+    assert_equal(5, HISTORY.length)
+    HISTORY.clear
+    assert_equal(0, HISTORY.length)
+  end
+
+  def test_empty_p
+    2.times do
+      assert(HISTORY.empty?)
+      HISTORY.push("s")
+      assert_equal(false, HISTORY.empty?)
+      HISTORY.clear
+      assert(HISTORY.empty?)
+    end
+  end
+
+  def test_delete_at
+    begin
+      lines = push_history(5)
+      (0..4).each do |i|
+        assert_equal(lines[i], HISTORY.delete_at(0))
+      end
+      assert(HISTORY.empty?)
+
+      lines = push_history(5)
+      (1..5).each do |i|
+        assert_equal(lines[lines.length - i], HISTORY.delete_at(-1))
+      end
+      assert(HISTORY.empty?)
+
+      lines = push_history(5)
+      assert_equal(lines[0], HISTORY.delete_at(0))
+      assert_equal(lines[4], HISTORY.delete_at(3))
+      assert_equal(lines[1], HISTORY.delete_at(0))
+      assert_equal(lines[3], HISTORY.delete_at(1))
+      assert_equal(lines[2], HISTORY.delete_at(0))
+      assert(HISTORY.empty?)
+    rescue NotImplementedError
+    end
+  end
+
+  def test_delete_at__out_of_range
+    assert_raises(IndexError, NotImplementedError, "index=<0>") do
+      HISTORY.delete_at(0)
+    end
+      
+    lines = push_history(5)
+    invalid_indexes = [5, 6, 100, -6, -7, -100]
+    invalid_indexes.each do |i|
+      assert_raises(IndexError, NotImplementedError, "index=<#{i}>") do
+        HISTORY.delete_at(i)
+      end
+    end
+    
+    invalid_indexes = [100_000_000_000_000_000_000,
+                       -100_000_000_000_000_000_000]
+    invalid_indexes.each do |i|
+      assert_raises(RangeError, NotImplementedError, "index=<#{i}>") do
+        HISTORY.delete_at(i)
+      end
+    end
+  end
+
+  private
+
+  def push_history(num)
+    lines = []
+    num.times do |i|
+      s = "a"
+      i.times do
+        s = s.succ
+      end
+      lines.push("#{i + 1}:#{s}")
+    end
+    HISTORY.push(*lines)
+    return lines
+  end
+end if defined?(::Readline) && defined?(::Readline::HISTORY) &&
+  (
+   begin
+     Readline::HISTORY.clear
+   rescue NotImplementedError
+     false
+   end
+   )

07/30/2008 03:28 PM - Kouji Takao

高尾宏治です。

On 2008/07/24, at 9:37, Takao Kouji wrote:
> On 2008/07/23, at 23:14, Takao Kouji wrote:
>> ただ、Readlineモジュールの入力履歴を扱うメソッドは、
>> 例外NotImplementedErrorが発生するかもしれないので、
>> それに対応したテストを記述する必要がありました。
>> 現在、この作業中です。
>>
>> この作業が終わったら、パッチを本MLに報告する予定です。
>
> テストを修正しました。
> 本メールの下部にパッチを記述しています。

いろいろな環境でテストしようと考えたのですが、
なかなか時間が取れませんでした。
とりあえず、 Mac OSX 10.5の環境で
libeditと GNU Readline 5.2 で動作を確認しています。
trunk であれば先のパッチを適用後、コミットしていただいて、
広くいろんな方に使っていただいても良いように思いますが、
いかがでしょうか。 > 前田さん

# 本当は私がいろいろな環境で試していただけるようにしたかったのですが、
# 思ったよりも時間は取れないもののですね。。。

Readline::HISTORYのtestを追加しているため、
先のパッチをコミットしていたければ、
それなりにいろいろな環境でテストしていいただける可能性があると考えています。

それでは、ご検討よろしくお願いいたします。


それと、私は今後もReadlineモジュールをメンテナンスしていく意志があります。
現在、亀のように遅いですが rdoc を記述しています。
Readlineの日本語のリファレンスマニュアルは私が記述しました。

  http://doc.loveruby.net/refm/api/view/class/Readline

Readlineモジュールのメンテナとして検討していただければ幸いです。

07/31/2008 11:00 PM - Kouji Takao

高尾宏治です。

On 2008/07/31, at 10:05, M.Suzuki wrote:
> Mac OSX 10.4で新しいパッチをテストしてみました。

ありがとうございます。

> msuzuki$ ./ruby --version
> ruby 1.9.0 (2008-07-31 revision 0) [powerpc-darwin8.11.0]
>
> msuzuki$ ./ruby -I.ext/powerpc-darwin8.11.0 / -rreadline
> test/readline/test_readline_history.rb Loaded suite
> test/readline/test_readline_history Started
> ....F...F....F....
> Finished in 0.173369 seconds.
>
>  1) Failure:
> test_empty_p(Readline::TestHistory)
>    [test/readline/test_readline_history.rb:237:in `block in
> test_empty_p' test/readline/test_readline_history.rb:236:in `times'
>     test/readline/test_readline_history.rb:236:in `test_empty_p']:
> <false> is not true.
>
>  2) Failure:
> test_length(Readline::TestHistory)
> [test/readline/test_readline_history.rb:226]: <0> expected but was
> <5>.

なんか変な挙動をしていますね。
HISTORY.pushしたのにempty?がtrueのようですね。
つまり、libeditのadd_history関数を処理しても、
実際にはヒストリに追加されず、history_lengthが0のままのようです。
これは参った。これって対応できるのかな。

>  3) Failure:
> test_safe_level_4(Readline::TestHistory)
>    [test/readline/test_readline_history.rb:56:in `block in
> test_safe_level_4' test/readline/test_readline_history.rb:55:in `each'
>     test/readline/test_readline_history.rb:55:in `test_safe_level_4']:
> method=<[]=>.
> <SecurityError> exception expected but was
> Class: <NotImplementedError>
> Message: <"[]=() function is unimplemented on this machine">
> ---Backtrace---
> test/readline/test_readline_history.rb:60:in `[]='
> test/readline/test_readline_history.rb:60:in `block (3 levels) in
> test_safe_level_4'
> ---------------

上記はテストのバグでした。
情報ありがとうございました。

> やはり history 周りはダメっぽいですね。
> そもそもライブラリがおかしい様なので、どうしようもないのかも。

GNU Readlineをインストールしてくださいってことでもいいかもしれませんね。

> マニュアルの注意事項に載せておくとかFAQを作っておいた方が良いかも。

そうですね。
今、ReadlineのRDocを記述している最中なので、検討してみます。

> # ちなみに [ruby-dev:35645] のパッチは行頭の空白が失われている(?)ので
> # そのままでは patch に読ませられませんでした。
> # 大きなパッチではないので、+-以外の行にスペースを追加しましたが、
> # patch のオプションでなんとかなるのかな。。。

そうでしたか、失礼しました。
Mac OSX 10.5のMail.appを使用しているのですが、
コピペしたときにスペースが削られたのかもしれませんね。まいったな。

07/31/2008 11:04 PM - Yukihiro Matsumoto

まつもと ゆきひろです

In message "Re: [ruby-dev:35707] Re: [Ruby 1.8 - Bug #212] Issues with Readline in Mac OS X"
    on Thu, 31 Jul 2008 22:59:50 +0900, Takao Kouji <kouji@takao7.net> writes:

|> # ちなみに [ruby-dev:35645] のパッチは行頭の空白が失われている(?)ので
|> # そのままでは patch に読ませられませんでした。
|> # 大きなパッチではないので、+-以外の行にスペースを追加しましたが、
|> # patch のオプションでなんとかなるのかな。。。
|
|そうでしたか、失礼しました。
|Mac OSX 10.5のMail.appを使用しているのですが、
|コピペしたときにスペースが削られたのかもしれませんね。まいったな。

patch に -l オプションを付けると空白の違いを無視してくれます。

08/01/2008 01:57 PM - Kouji Takao

高尾宏治です。

On 2008/08/01, at 10:05, M.Suzuki wrote:
>> なんか変な挙動をしていますね。
>> HISTORY.pushしたのにempty?がtrueのようですね。
>> つまり、libeditのadd_history関数を処理しても、
>> 実際にはヒストリに追加されず、history_lengthが0のままのようです。
>> これは参った。これって対応できるのかな。
>
> [ruby-dev-35557] の結果からも、history_get()の挙動がおかしいので、仕方な
> いかと・・・
>
> # add_history()した結果、history_lengthが0になるだけマシとも...

たしかに仕方ないのかもしれません。

> rubyでラップして独自に履歴機能を実装するという手も有りますが、
> 10.4は過去のバージョンですし、積極的に対応しなくても良いのではないかと個
> 人的には思います。

そうかもしれませんね。
libeditのバージョンがかなり古いようですので、
未対応ということにしましょうかね。

また、単体テストも修正せず、
テストに失敗することで「このバージョンはヒストリの操作に対応していません」
ということが分かって良いとも考えられます。

08/01/2008 03:46 PM - Shugo Maeda

前田です。

2008/07/26 23:55 Takao Kouji <kouji@takao7.net>:
> それと、私は今後もReadlineモジュールをメンテナンスしていく意志があります。
> 現在、亀のように遅いですが rdoc を記述しています。
> Readlineの日本語のリファレンスマニュアルは私が記述しました。
>
>  http://doc.loveruby.net/refm/api/view/class/Readline
>
> Readlineモジュールのメンテナとして検討していただければ幸いです。

まつもとさんとも相談して高尾君に引き取ってもらうことにしました。
# ついでにできればcursesも…。
というわけで、アカウントが出来たら自分でcommitしてください。

-- 
Shugo Maeda

08/01/2008 10:02 PM - Kouji Takao

高尾宏治です。

On 2008/08/01, at 15:43, Shugo Maeda wrote:
> 2008/07/26 23:55 Takao Kouji <kouji@takao7.net>:
>> それと、私は今後もReadlineモジュールをメンテナンスしていく意志があります。
>> 現在、亀のように遅いですが rdoc を記述しています。
>> Readlineの日本語のリファレンスマニュアルは私が記述しました。
>>
>> http://doc.loveruby.net/refm/api/view/class/Readline
>>
>> Readlineモジュールのメンテナとして検討していただければ幸いです。
>
> まつもとさんとも相談して高尾君に引き取ってもらうことにしました。
> # ついでにできればcursesも…。
> というわけで、アカウントが出来たら自分でcommitしてください。

今日、前田さんにkoujiというアカウントを作成していただきました。
これからパッチを整理して、Mac OSXの環境と、
Debian GNU/Linux etch上でテストしたらコミットします。
# curses は今後の課題ということで...

あと、せっかくなので、簡単に自己紹介をしておきます。

アカウント名: kouji
名前: 高尾 宏治
所属: ネットワーク応用通信研究所
ひとこと:
会社では、目の前がまつもとさんの席で、
後ろが前田さんの席という贅沢(?)な環境で仕事をしています。
OSSに関する活動としてはGPassやsecurity-keeperを開発しています。
(開発していましたが正しいかも...)
あと、島根県松江市でRubyに関する勉強会を主催しています。

以上です。
先輩コミッタのみなさま、
これからよろしくお願いします。

08/02/2008 12:55 AM - Kouji Takao

高尾宏治です。

On 2008/08/01, at 22:01, Takao Kouji wrote:
> これからパッチを整理して、Mac OSXの環境と、
> Debian GNU/Linux etch上でテストしたらコミットします。

r18313にてコミットしました。

今回の不具合は、1.8系に対するバグ報告をもとに調査、対応しました。
1.8系へのバックポートの需要があるのではないかと思いますが、
1.8系へのバックポートについては、互換性などを考察して、
あらためて提案したいと考えています。

08/13/2008 12:59 AM - Kouji Takao

I fixed this bug in trunk's revision 18313.
Do you want to backport to 1.8?

08/13/2008 01:06 AM - Federico Builes

Kouji: I'm not able to follow the discussion in japanese but it'd be great if this change was backported.

08/14/2008 01:38 AM - Kouji Takao

I created the patch for ruby 1.8 branch. The patch was attached. Please try it.

NOTE: This patch applyed Ruby failed RubySpec for Readline::HISTORY.
Because your reported problem was specification in RubySpec. hmm.
 

08/16/2008 12:53 AM - Federico Builes

The issue with the specs has been fixed by Arthur Schreiber in Rubyspec:947a6dc29, thanks for the patches!

09/12/2008 05:49 PM - Yui NARUSE

  • Status changed from Open to Closed

Also available in: Atom PDF