Skip to content

Commit 8f5abcb

Browse files
tompngmatzbot
authored andcommitted
[ruby/reline] Allow utf-8 safe meta key mapping in inputrc
(ruby/reline#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. ruby/reline@9844b99c6e
1 parent 98620f6 commit 8f5abcb

File tree

2 files changed

+55
-40
lines changed

2 files changed

+55
-40
lines changed

lib/reline/config.rb

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
class Reline::Config
22
attr_reader :test_mode
33

4-
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}|./
4+
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}|./
55

66
class InvalidInputrc < RuntimeError
77
attr_accessor :file, :lineno
@@ -194,13 +194,14 @@ def read_lines(lines, file = nil)
194194
# value ignores everything after a space, raw_value does not.
195195
var, value, raw_value = $1.downcase, $2.partition(' ').first, $2
196196
bind_variable(var, value, raw_value)
197-
next
198-
when /\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o
199-
key, func_name = $1, $2
200-
func_name = func_name.split.first
201-
keystroke, func = bind_key(key, func_name)
202-
next unless keystroke
203-
@additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func)
197+
when /^\s*(?:M|Meta)-([a-zA-Z_])\s*:\s*(.*)\s*$/o
198+
bind_key("\"\\M-#$1\"", $2)
199+
when /^\s*(?:C|Control)-([a-zA-Z_])\s*:\s*(.*)\s*$/o
200+
bind_key("\"\\C-#$1\"", $2)
201+
when /^\s*(?:(?:C|Control)-(?:M|Meta)|(?:M|Meta)-(?:C|Control))-([a-zA-Z_])\s*:\s*(.*)\s*$/o
202+
bind_key("\"\\M-\\C-#$1\"", $2)
203+
when /^\s*("#{KEYSEQ_PATTERN}+")\s*:\s*(.*)\s*$/o
204+
bind_key($1, $2)
204205
end
205206
end
206207
unless if_stack.empty?
@@ -310,7 +311,12 @@ def retrieve_string(str)
310311
parse_keyseq(str).map { |c| c.chr(Reline.encoding_system_needs) }.join
311312
end
312313

313-
def bind_key(key, func_name)
314+
def bind_key(key, value)
315+
keystroke, func = parse_key_binding(key, value)
316+
@additional_key_bindings[@keymap_label].add(@keymap_prefix + keystroke, func) if keystroke
317+
end
318+
319+
def parse_key_binding(key, func_name)
314320
if key =~ /\A"(.*)"\z/
315321
keyseq = parse_keyseq($1)
316322
else
@@ -319,27 +325,19 @@ def bind_key(key, func_name)
319325
if func_name =~ /"(.*)"/
320326
func = parse_keyseq($1)
321327
else
322-
func = func_name.tr(?-, ?_).to_sym # It must be macro.
328+
func = func_name.split.first.tr(?-, ?_).to_sym # It must be macro.
323329
end
324330
[keyseq, func]
325331
end
326332

327333
def key_notation_to_code(notation)
328334
case notation
335+
when /(?:\\(?:C|Control)-\\(?:M|Meta)|\\(?:M|Meta)-\\(?:C|Control))-([A-Za-z_])/
336+
[?\e.ord, $1.ord % 32]
329337
when /\\(?:C|Control)-([A-Za-z_])/
330-
(1 + $1.downcase.ord - ?a.ord)
338+
($1.upcase.ord % 32)
331339
when /\\(?:M|Meta)-([0-9A-Za-z_])/
332-
modified_key = $1
333-
case $1
334-
when /[0-9]/
335-
?\M-0.bytes.first + (modified_key.ord - ?0.ord)
336-
when /[A-Z]/
337-
?\M-A.bytes.first + (modified_key.ord - ?A.ord)
338-
when /[a-z]/
339-
?\M-a.bytes.first + (modified_key.ord - ?a.ord)
340-
end
341-
when /\\(?:C|Control)-(?:M|Meta)-[A-Za-z_]/, /\\(?:M|Meta)-(?:C|Control)-[A-Za-z_]/
342-
# 129 M-^A
340+
[?\e.ord, $1.ord]
343341
when /\\(\d{1,3})/ then $1.to_i(8) # octal
344342
when /\\x(\h{1,2})/ then $1.to_i(16) # hexadecimal
345343
when "\\e" then ?\e.ord
@@ -359,11 +357,9 @@ def key_notation_to_code(notation)
359357
end
360358

361359
def parse_keyseq(str)
362-
ret = []
363-
str.scan(KEYSEQ_PATTERN) do
364-
ret << key_notation_to_code($&)
360+
str.scan(KEYSEQ_PATTERN).flat_map do |notation|
361+
key_notation_to_code(notation)
365362
end
366-
ret
367363
end
368364

369365
def reload

test/reline/test_config.rb

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -126,41 +126,46 @@ def test_invalid_keystroke
126126
end
127127

128128
def test_bind_key
129-
assert_equal ['input'.bytes, 'abcde'.bytes], @config.bind_key('"input"', '"abcde"')
129+
assert_equal ['input'.bytes, 'abcde'.bytes], @config.parse_key_binding('"input"', '"abcde"')
130130
end
131131

132132
def test_bind_key_with_macro
133133

134-
assert_equal ['input'.bytes, :abcde], @config.bind_key('"input"', 'abcde')
134+
assert_equal ['input'.bytes, :abcde], @config.parse_key_binding('"input"', 'abcde')
135135
end
136136

137137
def test_bind_key_with_escaped_chars
138-
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"')
138+
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"')
139139
end
140140

141141
def test_bind_key_with_ctrl_chars
142-
assert_equal ['input'.bytes, "\C-h\C-h".bytes], @config.bind_key('"input"', '"\C-h\C-H"')
143-
assert_equal ['input'.bytes, "\C-h\C-h".bytes], @config.bind_key('"input"', '"\Control-h\Control-H"')
142+
assert_equal ['input'.bytes, "\C-h\C-h\C-_".bytes], @config.parse_key_binding('"input"', '"\C-h\C-H\C-_"')
143+
assert_equal ['input'.bytes, "\C-h\C-h\C-_".bytes], @config.parse_key_binding('"input"', '"\Control-h\Control-H\Control-_"')
144144
end
145145

146146
def test_bind_key_with_meta_chars
147-
assert_equal ['input'.bytes, "\M-h\M-H".bytes], @config.bind_key('"input"', '"\M-h\M-H"')
148-
assert_equal ['input'.bytes, "\M-h\M-H".bytes], @config.bind_key('"input"', '"\Meta-h\Meta-H"')
147+
assert_equal ['input'.bytes, "\eh\eH\e_".bytes], @config.parse_key_binding('"input"', '"\M-h\M-H\M-_"')
148+
assert_equal ['input'.bytes, "\eh\eH\e_".bytes], @config.parse_key_binding('"input"', '"\Meta-h\Meta-H\M-_"')
149+
end
150+
151+
def test_bind_key_with_ctrl_meta_chars
152+
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-_"')
153+
assert_equal ['input'.bytes, "\e\C-h\e\C-_".bytes], @config.parse_key_binding('"input"', '"\Meta-\Control-h\Control-\Meta-_"')
149154
end
150155

151156
def test_bind_key_with_octal_number
152157
input = %w{i n p u t}.map(&:ord)
153-
assert_equal [input, "\1".bytes], @config.bind_key('"input"', '"\1"')
154-
assert_equal [input, "\12".bytes], @config.bind_key('"input"', '"\12"')
155-
assert_equal [input, "\123".bytes], @config.bind_key('"input"', '"\123"')
156-
assert_equal [input, "\123".bytes + '4'.bytes], @config.bind_key('"input"', '"\1234"')
158+
assert_equal [input, "\1".bytes], @config.parse_key_binding('"input"', '"\1"')
159+
assert_equal [input, "\12".bytes], @config.parse_key_binding('"input"', '"\12"')
160+
assert_equal [input, "\123".bytes], @config.parse_key_binding('"input"', '"\123"')
161+
assert_equal [input, "\123".bytes + '4'.bytes], @config.parse_key_binding('"input"', '"\1234"')
157162
end
158163

159164
def test_bind_key_with_hexadecimal_number
160165
input = %w{i n p u t}.map(&:ord)
161-
assert_equal [input, "\x4".bytes], @config.bind_key('"input"', '"\x4"')
162-
assert_equal [input, "\x45".bytes], @config.bind_key('"input"', '"\x45"')
163-
assert_equal [input, "\x45".bytes + '6'.bytes], @config.bind_key('"input"', '"\x456"')
166+
assert_equal [input, "\x4".bytes], @config.parse_key_binding('"input"', '"\x4"')
167+
assert_equal [input, "\x45".bytes], @config.parse_key_binding('"input"', '"\x45"')
168+
assert_equal [input, "\x45".bytes + '6'.bytes], @config.parse_key_binding('"input"', '"\x456"')
164169
end
165170

166171
def test_include
@@ -384,6 +389,20 @@ def test_additional_key_bindings
384389
assert_equal expected, registered_key_bindings(expected.keys)
385390
end
386391

392+
def test_unquoted_additional_key_bindings
393+
@config.read_lines(<<~'LINES'.lines)
394+
Meta-a: "Ma"
395+
Control-b: "Cb"
396+
Meta-Control-c: "MCc"
397+
Control-Meta-d: "CMd"
398+
M-C-e: "MCe"
399+
C-M-f: "CMf"
400+
LINES
401+
402+
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 }
403+
assert_equal expected, registered_key_bindings(expected.keys)
404+
end
405+
387406
def test_additional_key_bindings_with_nesting_and_comment_out
388407
@config.read_lines(<<~'LINES'.lines)
389408
#"ab": "AB"

0 commit comments

Comments
 (0)