Index: ext/readline/readline.c =================================================================== --- ext/readline/readline.c (revision 18548) +++ ext/readline/readline.c (working copy) @@ -28,6 +28,7 @@ #define TOLOWER(c) (isupper(c) ? tolower(c) : c) +#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; @@ -42,6 +43,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); @@ -518,12 +521,26 @@ return rb_str_new2("HISTORY"); } +static int +history_get_offset_history_base(offset) + int offset; +{ + return history_base + offset; +} + +static int +history_get_offset_0(offset) + int offset; +{ + return offset; +} + static VALUE hist_get(self, index) VALUE self; VALUE index; { - HIST_ENTRY *entry; + HIST_ENTRY *entry = NULL; int i; rb_secure(4); @@ -531,7 +548,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"); } @@ -545,7 +564,7 @@ VALUE str; { #ifdef HAVE_REPLACE_HISTORY_ENTRY - HIST_ENTRY *entry; + HIST_ENTRY *entry = NULL; int i; rb_secure(4); @@ -554,7 +573,9 @@ if (i < 0) { i += history_length; } - entry = replace_history_entry(i, RSTRING(str)->ptr, NULL); + if (i >= 0) { + entry = replace_history_entry(i, RSTRING_PTR(str), NULL); + } if (entry == NULL) { rb_raise(rb_eIndexError, "invalid index"); } @@ -649,7 +670,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)); @@ -691,6 +712,20 @@ } static VALUE +hist_clear(self) + 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(self, str) VALUE self; VALUE str; @@ -811,6 +846,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); @@ -822,9 +858,28 @@ 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 || defined HAVE_HAVE_REMOVE_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; + } +#if defined HAVE_CLEAR_HISTORY + clear_history(); #else + { + HIST_ENTRY *entry = remove_history(0); + free(entry->line); + free(entry); + } +#endif + } +#endif +#else rb_define_const(mReadline, "VERSION", rb_str_new2("2.0 or before version")); #endif Index: ext/readline/extconf.rb =================================================================== --- ext/readline/extconf.rb (revision 18548) +++ ext/readline/extconf.rb (working copy) @@ -65,4 +65,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,309 @@ +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, NotImplementedError, + "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=") 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 + e = HISTORY.each { |s| + assert_equal(HISTORY[i], s) + assert_equal(lines[i], s) + i += 1 + } + assert_equal(HISTORY, 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 + )