diff --git a/ext/readline/extconf.rb b/ext/readline/extconf.rb index 776ab19..dfa192f 100644 --- a/ext/readline/extconf.rb +++ b/ext/readline/extconf.rb @@ -81,6 +81,7 @@ def readline.have_type(type) readline.have_var("rl_editing_mode") readline.have_var("rl_line_buffer") readline.have_var("rl_point") +readline.have_var("rl_char_is_quoted_p") # workaround for native windows. /mswin|bccwin|mingw/ !~ RUBY_PLATFORM && readline.have_var("rl_event_hook") /mswin|bccwin|mingw/ !~ RUBY_PLATFORM && readline.have_var("rl_catch_sigwinch") diff --git a/ext/readline/readline.c b/ext/readline/readline.c index c3b98a8..b28ca14 100644 --- a/ext/readline/readline.c +++ b/ext/readline/readline.c @@ -59,6 +59,10 @@ static VALUE mReadline; #define COMPLETION_PROC "completion_proc" #define COMPLETION_CASE_FOLD "completion_case_fold" static ID completion_proc, completion_case_fold; +#if defined HAVE_RL_CHAR_IS_QUOTED_P +#define QUOTING_DETECTION_PROC "quoting_detection_proc" +static ID quoting_detection_proc; +#endif #if USE_INSERT_IGNORE_ESCAPE static ID id_orig_prompt, id_last_prompt; #endif @@ -88,6 +92,11 @@ static int readline_completion_append_character; static char **readline_attempted_completion_function(const char *text, int start, int end); +#ifdef HAVE_RL_CHAR_IS_QUOTED_P +int readline_char_is_quoted(char *text, int index); +long byte_index_to_char_index(VALUE str, long byte_index); +#endif + #define OutputStringValue(str) do {\ SafeStringValue(str);\ (str) = rb_str_conv_enc((str), rb_enc_get(str), rb_locale_encoding());\ @@ -832,6 +841,55 @@ readline_s_get_completion_proc(VALUE self) return rb_attr_get(mReadline, completion_proc); } +#ifdef HAVE_RL_CHAR_IS_QUOTED_P +/* + * call-seq: + * Readline.quoting_detection_proc = proc + * + * Specifies a Proc object +proc+ to determine if a character in the user's + * input is escaped. It should take the user's input and the index of the + * character in question as input, and return a boolean (true if the specified + * character is escaped). + * + * Readline will only call this proc with characters specified in + * +completer_quote_characters+, to discover if they indicate the end of a + * quoted argument, or characters specified in + * +completer_word_break_characters+, to discover if they indicate a break + * between arguments. + * + * If +completer_quote_characters+ is not set, or if the user input doesn't + * contain one of the +completer_quote_characters+ or a +\+ character, + * Readline will not attempt to use this proc at all. + * + * Raises ArgumentError if +proc+ does not respond to the call method. + */ +static VALUE +readline_s_set_quoting_detection_proc(VALUE self, VALUE proc) +{ + if (!NIL_P(proc) && !rb_respond_to(proc, rb_intern("call"))) + rb_raise(rb_eArgError, "argument must respond to `call'"); + return rb_ivar_set(mReadline, quoting_detection_proc, proc); +} +#else +#define readline_s_set_quoting_detection_proc rb_f_notimplement +#endif + +#ifdef HAVE_RL_CHAR_IS_QUOTED_P +/* + * call-seq: + * Readline.quoting_detection_proc -> proc + * + * Returns the quoting detection Proc object. + */ +static VALUE +readline_s_get_quoting_detection_proc(VALUE self) +{ + return rb_attr_get(mReadline, quoting_detection_proc); +} +#else +#define readline_s_get_quoting_detection_proc rb_f_notimplement +#endif + /* * call-seq: * Readline.completion_case_fold = bool @@ -1007,6 +1065,51 @@ readline_attempted_completion_function(const char *text, int start, int end) return result; } +#ifdef HAVE_RL_CHAR_IS_QUOTED_P +int +readline_char_is_quoted(char *text, int byte_index) +{ + VALUE proc, result, str; + long char_index; + + proc = rb_attr_get(mReadline, quoting_detection_proc); + if (NIL_P(proc)) { + return 0; + } + + str = rb_locale_str_new_cstr(text); + char_index = byte_index_to_char_index(str, (long)byte_index); + + if (char_index == -1) { + rb_raise(rb_eIndexError, "failed to find character at byte index"); + } + + result = rb_funcall(proc, rb_intern("call"), 2, str, LONG2FIX(char_index)); + return result ? 1 : 0; +} + +long +byte_index_to_char_index(VALUE str, long byte_index) +{ + const char *ptr; + long ci, bi, len, clen; + rb_encoding *enc; + + enc = rb_enc_get(str); + len = RSTRING_LEN(str); + ptr = RSTRING_PTR(str); + + for (bi = 0, ci = 0; bi < len; bi += clen, ++ci) { + if (bi == byte_index) { + return ci; + } + clen = rb_enc_mbclen(ptr + bi, ptr + len, enc); + } + + return -1; +} +#endif + #ifdef HAVE_RL_SET_SCREEN_SIZE /* * call-seq: @@ -1821,6 +1924,9 @@ Init_readline(void) #if defined(HAVE_RL_SPECIAL_PREFIXES) id_special_prefixes = rb_intern("special_prefixes"); #endif +#if defined HAVE_RL_CHAR_IS_QUOTED_P + quoting_detection_proc = rb_intern(QUOTING_DETECTION_PROC); +#endif mReadline = rb_define_module("Readline"); rb_define_module_function(mReadline, "readline", @@ -1833,6 +1939,10 @@ Init_readline(void) readline_s_set_completion_proc, 1); rb_define_singleton_method(mReadline, "completion_proc", readline_s_get_completion_proc, 0); + rb_define_singleton_method(mReadline, "quoting_detection_proc=", + readline_s_set_quoting_detection_proc, 1); + rb_define_singleton_method(mReadline, "quoting_detection_proc", + readline_s_get_quoting_detection_proc, 0); rb_define_singleton_method(mReadline, "completion_case_fold=", readline_s_set_completion_case_fold, 1); rb_define_singleton_method(mReadline, "completion_case_fold", @@ -1981,6 +2091,9 @@ Init_readline(void) #if defined(HAVE_RL_PRE_INPUT_HOOK) rl_pre_input_hook = (rl_hook_func_t *)readline_pre_input_hook; #endif +#if defined HAVE_RL_CHAR_IS_QUOTED_P + rl_char_is_quoted_p = &readline_char_is_quoted; +#endif #ifdef HAVE_RL_CATCH_SIGNALS rl_catch_signals = 0; #endif diff --git a/test/readline/test_readline.rb b/test/readline/test_readline.rb index eae9518..8b98b62 100644 --- a/test/readline/test_readline.rb +++ b/test/readline/test_readline.rb @@ -464,6 +464,92 @@ def test_refresh_line end end if Readline.respond_to?(:refresh_line) + def test_setting_quoting_detection_proc + return unless Readline.respond_to?(:quoting_detection_proc=) + + expected = proc { |text, index| false } + Readline.quoting_detection_proc = expected + assert_equal(expected, Readline.quoting_detection_proc) + + assert_raise(ArgumentError) do + Readline.quoting_detection_proc = "This does not have call method." + end + end + + def test_using_quoting_detection_proc + saved_completer_quote_characters = Readline.completer_quote_characters + saved_completer_word_break_characters = Readline.completer_word_break_characters + return unless Readline.respond_to?(:quoting_detection_proc=) + + passed_text = nil + line = nil + + with_temp_stdio do |stdin, stdout| + replace_stdio(stdin.path, stdout.path) do + Readline.completion_proc = -> (text) do + passed_text = text + ['completion'] + end + Readline.completer_quote_characters = '\'"' + Readline.completer_word_break_characters = ' ' + Readline.quoting_detection_proc = -> (text, index) do + index > 0 && text[index-1] == '\\' + end + + stdin.write("first second\\ third\t") + stdin.flush + line = Readline.readline('> ', false) + end + end + + assert_equal('second\\ third', passed_text) + assert_equal('first completion', line) + ensure + Readline.completer_quote_characters = saved_completer_quote_characters + Readline.completer_word_break_characters = saved_completer_word_break_characters + end + + def test_using_quoting_detection_proc_with_multibyte_input + saved_completer_quote_characters = Readline.completer_quote_characters + saved_completer_word_break_characters = Readline.completer_word_break_characters + return unless Readline.respond_to?(:quoting_detection_proc=) + unless Encoding.find("locale") == Encoding::UTF_8 + return if assert_under_utf8 + skip 'this test needs UTF-8 locale' + end + + passed_text = nil + escaped_char_indexes = [] + line = nil + + with_temp_stdio do |stdin, stdout| + replace_stdio(stdin.path, stdout.path) do + Readline.completion_proc = -> (text) do + passed_text = text + ['completion'] + end + Readline.completer_quote_characters = '\'"' + Readline.completer_word_break_characters = ' ' + Readline.quoting_detection_proc = -> (text, index) do + escaped = index > 0 && text[index-1] == '\\' + escaped_char_indexes << index if escaped + escaped + end + + stdin.write("\u3042\u3093 second\\ third\t") + stdin.flush + line = Readline.readline('> ', false) + end + end + + assert_equal([10], escaped_char_indexes) + assert_equal('second\\ third', passed_text) + assert_equal("\u3042\u3093 completion", line) + ensure + Readline.completer_quote_characters = saved_completer_quote_characters + Readline.completer_word_break_characters = saved_completer_word_break_characters + end + private def replace_stdio(stdin_path, stdout_path)