rb-readlineと^Y
Gemfileにrb-readlineがあるとirbやpryで困ったことになる。入力中にうっかり^Y
(Ctrl
+Y
)をタイプすると、いきなりプロセスがサスペンドしてしまうのだ。
^Z
でプロセスをサスペンドさせるのはおなじみのジョブ制御であり、このときにはプロセスはシグナルSIGSTOPを受けることでサスペンドさせられることになる。
^Y
でもプロセスをサスペンドさせられるのだが^Z
とは異なる点がある。まず、^Y
は入力中にだけ有効である。そして、プロセスが受けるシグナルはSIGTSTPである。(参考: termios(3)のVDSUSPの説明)
ただ、この^Y
によるジョブ制御に対応しているのはmacOSやSolarisなど限られた環境だけらしい。stty -a
の出力にdsusp = ^Y
が含まれていれば^Y
に対応している。
もっともSIGTSTPはプロセス側でシグナル処理を定義することができるので、実行中のプロセスによっては^Y
によるサスペンドができない場合がある。たとえば日常的に使用する各種シェルがそうで、bashにしろzshにしろ一般的なキーバインド(emacs)での^Y
はyankであり、^K
でカットした文字が貼り付けられることになる。
さて、問題のrb-readlineがどうかというと、SIGTSTPへの対処は特にしていないようだ。たとえばirbで^Y
をタイプするとサスペンドしてしまうのでyankができないわけだが、^V^Y
とタイプするとyankすることができる。
で、実は、そのあたりの動作は^Y
のことに気付いてしまえばfg
で戻るなどすればどうとでもなる、はずなのだが、どうした具合いなのか、キー入力をrb-readlineとシェルとで取り合うようなかっこうになることがある。単純にrb-readlineとirbを組み合わせるだけでは再現できなくて、手元では、たとえばrails consoleなどでそういうことが起きる場合がある。それも常にというわけではないのだが、ともあれば、一度そうなってしまうとプロセスをkillするか、端末を終了させないとどうにもならなくなってとてもめんどくさい。
というわけで、回避コードを考えてみた。
~/.pryrc:
# macOSでのrb-readline使用時の"\C-y"サスペンド問題の回避
stty_ccs = {}
nest_level = -1
Pry.hooks.add_hook(:when_started, 'avoid dsusp') do
if defined?(::RbReadline) && $".grep(/\/rbreadline.rb\z/)
stty = `stty -a`
stty_ccs = { 'dsusp' => '^Y', 'lnext' => '^V' }
stty_ccs.delete_if {|cc, key| /\b#{Regexp.quote("#{cc} = #{key}")};/ !~ stty }
end
end
Pry.hooks.add_hook(:before_session, 'count nest level') do
nest_level += 1
end
Pry.hooks.add_hook(:before_session, 'avoid dsusp') do |_, _, pry|
system('stty', *stty_ccs.keys.inject([]) {|opts, cc| opts << cc << 'undef' }) if nest_level.zero? && !stty_ccs.empty?
end
Pry.hooks.add_hook(:after_session, 'avoid dsusp') do |_, _, pry|
system('stty', *stty_ccs.to_a.flatten) if nest_level.zero? && !stty_ccs.empty?
end
Pry.hooks.add_hook(:after_session, 'count nest level') do
nest_level -= 1
end
~/.irbrc:
# macOSでのrb-readline使用時の"\C-y"サスペンド問題の回避
if defined?(::RbReadline) && $".grep(/\/rbreadline.rb\z/)
stty = `stty -a`
stty_ccs = { 'dsusp' => '^Y', 'lnext' => '^V' }
stty_ccs.delete_if {|cc, key| /\b#{Regexp.quote("#{cc} = #{key}")};/ !~ stty }
unless stty_ccs.empty?
system('stty', *stty_ccs.keys.inject([]) {|opts, cc| opts << cc << 'undef' })
IRB.conf[:AT_EXIT] << proc do
system('stty', *stty_ccs.to_a.flatten)
end
end
end
Pryによる~/.pryrcの読み込みは一度だけなので、Pryセッションを複数回開くケースを考えるとフックを使って制御する必要がある。
IRBはbinding.irb
が呼ばれるたびに~/.irbrcが読み込まれる。その際にirbの設定(IRB.conf
)が初期化されるので、Pryと比較するとわりとベタっと書いてやるだけでよい。
ついでにいろいろ確認したときのテストコード:
require 'pty'
require 'timeout'
irb_start = [
'gem "rb-readline"', # rb-readlineを使用しない場合はコメントアウト
'require "irb"',
'IRB.start',
].join('; ')
timeout = 5
bash_prompt = /bash-[.\d]+\$ \z/
irb_prompt = />> \z/
ios = [
bash_prompt,
"echo cat test\n",
bash_prompt,
"cat\n",
"aaa\n",
"bbb\n",
"\C-d",
bash_prompt,
"echo readline test\n",
bash_prompt,
"ruby -e '#{irb_start}' -- -f --prompt-mode=simple\n",
irb_prompt,
":ccc\n",
irb_prompt,
":ddd", "\C-a", "\C-k", "eee = ", "\C-y", "\n",
irb_prompt,
":fff\n",
irb_prompt,
"quit\n",
bash_prompt,
"echo done.\n",
bash_prompt,
"exit\n",
]
result = 'OK'
wait = nil
buf = ''
PTY.spawn('bash --norc') do |r, w, _p|
w.sync = true
begin
Timeout.timeout(timeout) do
ios.each do |obj|
if obj.is_a?(String)
w.write obj
$stderr.write '+'
next
end
wait = obj
loop do
buf << r.readpartial(1024)
break if wait.match(buf)
$stderr.write '-'
sleep 0.1
end
$stderr.write '.'
end
end
buf << r.readpartial(1024)
rescue Timeout::Error
result = "ERROR: couldn't get #{wait.inspect}"
ensure
w.close
r.close
end
end
$stderr.write "\n#{'-' * 78}\n"
print "RESULT = #{result}\n\n#{buf}"
^Y
はmacOSなどでしか使えないのでLinux環境では動作しない。また、端末制御の関係なのかSSH経由でもうまく動作しない場合がある。(vagrant sshとか)
rb-readlineなしの場合(通常期待される動作):
$ ruby test.rb
.+-.++++-.+-.+--.+--.++++++---.+--.+-.+-.+
------------------------------------------------------------------------------
RESULT = OK
bash-3.2$ echo cat test
cat test
bash-3.2$ cat
aaa
bbb
aaa
bbb
bash-3.2$ echo readline test
readline test
bash-3.2$ ruby -e 'require "irb"; IRB.start' -- -f --prompt-mode=simple
>> :ccc
=> :ccc
>> eee = :ddd
=> :ddd
>> :fff
=> :fff
>> quit
bash-3.2$ echo done.
done.
rb-readlineありの場合:
$ ruby test.rb
-.+-.++++-.+-.+--.+-.++++++---
------------------------------------------------------------------------------
RESULT = ERROR: couldn't get />> \z/
bash-3.2$ echo cat test
cat test
bash-3.2$ cat
aaa
bbb
aaa
bbb
bash-3.2$ echo readline test
readline test
ode=simpleruby -e 'gem "rb-readline"; require "irb"; IRB.start' -- -f --prompt-m
>> :ccc
=> :ccc
>> eee =
[1]+ Stopped ruby -e 'gem "rb-readline"; require "irb"; IRB.start' -- -f --prompt-mode=simple
bash-3.2$