From 9844b99c6eb0d84ae24bd85509ffab75cccee544 Mon Sep 17 00:00:00 2001 From: tomoya ishida Date: Mon, 7 Oct 2024 08:35:03 +0900 Subject: [PATCH] Allow utf-8 safe meta key mapping in inputrc (#723) Readline's convert-meta setting is utf-8 unsafe. Allow assigning `"\M-char": key` to bind "\echar": key even if convert-meta is not enabled. --- lib/reline/config.rb | 48 +++++++++++++++++--------------------- test/reline/test_config.rb | 47 ++++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/lib/reline/config.rb b/lib/reline/config.rb index 6aa6ba8d94..e0fc37fc68 100644 --- a/lib/reline/config.rb +++ b/lib/reline/config.rb @@ -1,7 +1,7 @@ class Reline::Config attr_reader :test_mode - KEYSEQ_PATTERN = /\\(?:C|Control)-[A-Za-z_]|\\(?:M|Meta)-[0-9A-Za-z_]|\\(?:C|Control)-(?:M|Meta)-[A-Za-z_]|\\(?:M|Meta)-(?:C|Control)-[A-Za-z_]|\\e|\\[\\\"\'abdfnrtv]|\\\d{1,3}|\\x\h{1,2}|./ + KEYSEQ_PATTERN = /\\(?:C|Control)-[A-Za-z_]|\\(?:M|Meta)-[0-9A-Za-z_]|\\(?:C|Control)-\\(?:M|Meta)-[A-Za-z_]|\\(?:M|Meta)-\\(?:C|Control)-[A-Za-z_]|\\e|\\[\\\"\'abdfnrtv]|\\\d{1,3}|\\x\h{1,2}|./ class InvalidInputrc < RuntimeError attr_accessor :file, :lineno @@ -194,13 +194,14 @@ def read_lines(lines, file = nil) # value ignores everything after a space, raw_value does not. var, value, raw_value = $1.downcase, $2.partition(' ').first, $2 bind_variable(var, value, raw_value) - next - when /\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o - key, func_name = $1, $2 - func_name = func_name.split.first - keystroke, func = bind_key(key, func_name) - next unless keystroke - @additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func) + when /^\s*(?:M|Meta)-([a-zA-Z_])\s*:\s*(.*)\s*$/o + bind_key("\"\\M-#$1\"", $2) + when /^\s*(?:C|Control)-([a-zA-Z_])\s*:\s*(.*)\s*$/o + bind_key("\"\\C-#$1\"", $2) + when /^\s*(?:(?:C|Control)-(?:M|Meta)|(?:M|Meta)-(?:C|Control))-([a-zA-Z_])\s*:\s*(.*)\s*$/o + bind_key("\"\\M-\\C-#$1\"", $2) + when /^\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o + bind_key($1, $2) end end unless if_stack.empty? @@ -310,7 +311,12 @@ def retrieve_string(str) parse_keyseq(str).map { |c| c.chr(Reline.encoding_system_needs) }.join end - def bind_key(key, func_name) + def bind_key(key, value) + keystroke, func = parse_key_binding(key, value) + @additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func) if keystroke + end + + def parse_key_binding(key, func_name) if key =~ /\A"(.*)"\z/ keyseq = parse_keyseq($1) else @@ -319,27 +325,19 @@ def bind_key(key, func_name) if func_name =~ /"(.*)"/ func = parse_keyseq($1) else - func = func_name.tr(?-, ?_).to_sym # It must be macro. + func = func_name.split.first.tr(?-, ?_).to_sym # It must be macro. end [keyseq, func] end def key_notation_to_code(notation) case notation + when /(?:\\(?:C|Control)-\\(?:M|Meta)|\\(?:M|Meta)-\\(?:C|Control))-([A-Za-z_])/ + [?\e.ord, $1.ord % 32] when /\\(?:C|Control)-([A-Za-z_])/ - (1 + $1.downcase.ord - ?a.ord) + ($1.upcase.ord % 32) when /\\(?:M|Meta)-([0-9A-Za-z_])/ - modified_key = $1 - case $1 - when /[0-9]/ - ?\M-0.bytes.first + (modified_key.ord - ?0.ord) - when /[A-Z]/ - ?\M-A.bytes.first + (modified_key.ord - ?A.ord) - when /[a-z]/ - ?\M-a.bytes.first + (modified_key.ord - ?a.ord) - end - when /\\(?:C|Control)-(?:M|Meta)-[A-Za-z_]/, /\\(?:M|Meta)-(?:C|Control)-[A-Za-z_]/ - # 129 M-^A + [?\e.ord, $1.ord] when /\\(\d{1,3})/ then $1.to_i(8) # octal when /\\x(\h{1,2})/ then $1.to_i(16) # hexadecimal when "\\e" then ?\e.ord @@ -359,11 +357,9 @@ def key_notation_to_code(notation) end def parse_keyseq(str) - ret = [] - str.scan(KEYSEQ_PATTERN) do - ret << key_notation_to_code($&) + str.scan(KEYSEQ_PATTERN).flat_map do |notation| + key_notation_to_code(notation) end - ret end def reload diff --git a/test/reline/test_config.rb b/test/reline/test_config.rb index 68a102a599..878477fe5e 100644 --- a/test/reline/test_config.rb +++ b/test/reline/test_config.rb @@ -126,41 +126,46 @@ def test_invalid_keystroke end def test_bind_key - assert_equal ['input'.bytes, 'abcde'.bytes], @config.bind_key('"input"', '"abcde"') + assert_equal ['input'.bytes, 'abcde'.bytes], @config.parse_key_binding('"input"', '"abcde"') end def test_bind_key_with_macro - assert_equal ['input'.bytes, :abcde], @config.bind_key('"input"', 'abcde') + assert_equal ['input'.bytes, :abcde], @config.parse_key_binding('"input"', 'abcde') end def test_bind_key_with_escaped_chars - assert_equal ['input'.bytes, "\e \\ \" ' \a \b \d \f \n \r \t \v".bytes], @config.bind_key('"input"', '"\\e \\\\ \\" \\\' \\a \\b \\d \\f \\n \\r \\t \\v"') + assert_equal ['input'.bytes, "\e \\ \" ' \a \b \d \f \n \r \t \v".bytes], @config.parse_key_binding('"input"', '"\\e \\\\ \\" \\\' \\a \\b \\d \\f \\n \\r \\t \\v"') end def test_bind_key_with_ctrl_chars - assert_equal ['input'.bytes, "\C-h\C-h".bytes], @config.bind_key('"input"', '"\C-h\C-H"') - assert_equal ['input'.bytes, "\C-h\C-h".bytes], @config.bind_key('"input"', '"\Control-h\Control-H"') + assert_equal ['input'.bytes, "\C-h\C-h\C-_".bytes], @config.parse_key_binding('"input"', '"\C-h\C-H\C-_"') + assert_equal ['input'.bytes, "\C-h\C-h\C-_".bytes], @config.parse_key_binding('"input"', '"\Control-h\Control-H\Control-_"') end def test_bind_key_with_meta_chars - assert_equal ['input'.bytes, "\M-h\M-H".bytes], @config.bind_key('"input"', '"\M-h\M-H"') - assert_equal ['input'.bytes, "\M-h\M-H".bytes], @config.bind_key('"input"', '"\Meta-h\Meta-H"') + assert_equal ['input'.bytes, "\eh\eH\e_".bytes], @config.parse_key_binding('"input"', '"\M-h\M-H\M-_"') + assert_equal ['input'.bytes, "\eh\eH\e_".bytes], @config.parse_key_binding('"input"', '"\Meta-h\Meta-H\M-_"') + end + + def test_bind_key_with_ctrl_meta_chars + assert_equal ['input'.bytes, "\e\C-h\e\C-h\e\C-_".bytes], @config.parse_key_binding('"input"', '"\M-\C-h\C-\M-H\M-\C-_"') + assert_equal ['input'.bytes, "\e\C-h\e\C-_".bytes], @config.parse_key_binding('"input"', '"\Meta-\Control-h\Control-\Meta-_"') end def test_bind_key_with_octal_number input = %w{i n p u t}.map(&:ord) - assert_equal [input, "\1".bytes], @config.bind_key('"input"', '"\1"') - assert_equal [input, "\12".bytes], @config.bind_key('"input"', '"\12"') - assert_equal [input, "\123".bytes], @config.bind_key('"input"', '"\123"') - assert_equal [input, "\123".bytes + '4'.bytes], @config.bind_key('"input"', '"\1234"') + assert_equal [input, "\1".bytes], @config.parse_key_binding('"input"', '"\1"') + assert_equal [input, "\12".bytes], @config.parse_key_binding('"input"', '"\12"') + assert_equal [input, "\123".bytes], @config.parse_key_binding('"input"', '"\123"') + assert_equal [input, "\123".bytes + '4'.bytes], @config.parse_key_binding('"input"', '"\1234"') end def test_bind_key_with_hexadecimal_number input = %w{i n p u t}.map(&:ord) - assert_equal [input, "\x4".bytes], @config.bind_key('"input"', '"\x4"') - assert_equal [input, "\x45".bytes], @config.bind_key('"input"', '"\x45"') - assert_equal [input, "\x45".bytes + '6'.bytes], @config.bind_key('"input"', '"\x456"') + assert_equal [input, "\x4".bytes], @config.parse_key_binding('"input"', '"\x4"') + assert_equal [input, "\x45".bytes], @config.parse_key_binding('"input"', '"\x45"') + assert_equal [input, "\x45".bytes + '6'.bytes], @config.parse_key_binding('"input"', '"\x456"') end def test_include @@ -384,6 +389,20 @@ def test_additional_key_bindings assert_equal expected, registered_key_bindings(expected.keys) end + def test_unquoted_additional_key_bindings + @config.read_lines(<<~'LINES'.lines) + Meta-a: "Ma" + Control-b: "Cb" + Meta-Control-c: "MCc" + Control-Meta-d: "CMd" + M-C-e: "MCe" + C-M-f: "CMf" + LINES + + expected = { "\ea".bytes => 'Ma'.bytes, "\C-b".bytes => 'Cb'.bytes, "\e\C-c".bytes => 'MCc'.bytes, "\e\C-d".bytes => 'CMd'.bytes, "\e\C-e".bytes => 'MCe'.bytes, "\e\C-f".bytes => 'CMf'.bytes } + assert_equal expected, registered_key_bindings(expected.keys) + end + def test_additional_key_bindings_with_nesting_and_comment_out @config.read_lines(<<~'LINES'.lines) #"ab": "AB"