在 Ruby 中检测没有 getc/gets 的按键(非阻塞)
Posted
技术标签:
【中文标题】在 Ruby 中检测没有 getc/gets 的按键(非阻塞)【英文标题】:Detect key press (non-blocking) w/o getc/gets in Ruby 【发布时间】:2010-10-31 02:39:32 【问题描述】:我有一个简单的任务,需要等待文件系统发生变化(它本质上是一个原型编译器)。所以我有一个简单的无限循环,在检查更改的文件后休眠 5 秒。
loop do
# if files changed
# process files
# and puts result
sleep 5
end
而不是Ctrl+C
敬礼,我宁愿能够测试并查看是否已按下某个键,而不会阻塞循环。本质上,我只需要一种方法来判断是否有传入的按键,然后抓住它们直到遇到 Q,然后退出程序。
我想要的是:
def wait_for_Q
key_is_pressed && get_ch == 'Q'
end
loop do
# if files changed
# process files
# and puts result
wait_for_Q or sleep 5
end
或者,这是 Ruby 不擅长的(很好)吗?
【问题讨论】:
【参考方案1】:这是一种方法,使用IO#read_nonblock
:
def quit?
begin
# See if a 'Q' has been typed yet
while c = STDIN.read_nonblock(1)
puts "I found a #c"
return true if c == 'Q'
end
# No 'Q' found
false
rescue Errno::EINTR
puts "Well, your device seems a little slow..."
false
rescue Errno::EAGAIN
# nothing was ready to be read
puts "Nothing to be read..."
false
rescue EOFError
# quit on the end of the input stream
# (user hit CTRL-D)
puts "Who hit CTRL-D, really?"
true
end
end
loop do
puts "I'm a loop!"
puts "Checking to see if I should quit..."
break if quit?
puts "Nope, let's take a nap"
sleep 5
puts "Onto the next iteration!"
end
puts "Oh, I quit."
请记住,即使这使用非阻塞 IO,它仍然是 缓冲 IO。
这意味着您的用户必须先点击Q
,然后再点击<Enter>
。如果你想做
无缓冲 IO,我建议查看 ruby 的 curses 库。
【讨论】:
遗憾的是,我在 Windows 上,这会引发 Errno::EBADF 或错误文件错误。我会调查我的选择。 尝试使用 EINTR 和 EAGAIN 捕获 EBADF - 这可能只是一个暂时性错误,直到您实际输入一些输入(不确定,不是在 Windows 上) 是的。 Ruby 只是使用 read(参见 man 2 read)来做到这一点,它是原生 C,而 php 或 Perl 无疑是 wrap。IO.console.raw|c| c.read_nonblock(1) rescue nil
帮了我大忙。【参考方案2】:
其他答案的组合可以获得所需的行为。在 OSX 和 Linux 上的 ruby 1.9.3 中测试。
loop do
puts 'foo'
system("stty raw -echo")
char = STDIN.read_nonblock(1) rescue nil
system("stty -raw echo")
break if /q/i =~ char
sleep(2)
end
【讨论】:
虽然这个答案很有用,但应该注意的是,它并没有捕捉到@rampion 的答案捕捉到的所有错误,而且这些错误并不罕见。【参考方案3】:您也可以在没有缓冲区的情况下执行此操作。在基于 unix 的系统中,这很容易:
system("stty raw -echo") #=> Raw mode, no echo
char = STDIN.getc
system("stty -raw echo") #=> Reset terminal mode
puts char
这将等待按键被按下并返回字符代码。无需按。
将char = STDIN.getc
放入一个循环中就可以了!
如果你在 Windows 上,根据 The Ruby Way,你需要用 C 编写一个扩展或者使用这个小技巧(虽然这是在 2001 年编写的,所以可能有更好的方法)
require 'Win32API'
char = Win32API.new('crtdll','_getch', [], 'L').Call
这是我的参考资料:great book, if you don't own it you should
【讨论】:
我不明白。这是如何无阻塞的?它等待字符。【参考方案4】:通过结合我刚刚阅读的各种解决方案,我想出了一个跨平台的方法来解决这个问题。
Details here,但这里是相关的代码:GetKey.getkey
方法返回 ASCII 代码或 nil
,如果没有按下。
应该在 Windows 和 Unix 上都可以工作。
module GetKey
# Check if Win32API is accessible or not
@use_stty = begin
require 'Win32API'
false
rescue LoadError
# Use Unix way
true
end
# Return the ASCII code last key pressed, or nil if none
#
# Return::
# * _Integer_: ASCII code of the last key pressed, or nil if none
def self.getkey
if @use_stty
system('stty raw -echo') # => Raw mode, no echo
char = (STDIN.read_nonblock(1).ord rescue nil)
system('stty -raw echo') # => Reset terminal mode
return char
else
return Win32API.new('crtdll', '_kbhit', [ ], 'I').Call.zero? ? nil : Win32API.new('crtdll', '_getch', [ ], 'L').Call
end
end
end
这是一个简单的测试程序:
loop do
k = GetKey.getkey
puts "Key pressed: #k.inspect"
sleep 1
end
在上面提供的链接中,我还展示了如何使用 curses
库,但结果在 Windows 上有点奇怪。
【讨论】:
你的链接失效了 链接备份。谢谢【参考方案5】:您可能还想研究 Ruby 的“io/wait”库,它为所有 IO 对象提供了ready?
方法。我没有专门测试过你的情况,但是我在我正在研究的基于套接字的库中使用它。在您的情况下,如果 STDIN 只是一个标准 IO 对象,您可能会在ready?
返回非零结果时退出,除非您有兴趣找出实际按下的键。这个功能可以通过require 'io/wait'
获得,它是Ruby 标准库的一部分。我不确定它是否适用于所有环境,但值得一试。研究人员:http://ruby-doc.org/stdlib/libdoc/io/wait/rdoc/
【讨论】:
【参考方案6】:现在用这个
require 'Win32API'
VK_SHIFT = 0x10
VK_ESC = 0x1B
def check_shifts()
$listener.call(VK_SHIFT) != 0 ? true : false
end
# create empty Hash of key codes
keys = Hash.new
# create empty Hash for shift characters
uppercase = Hash.new
# add letters
(0x41..0x5A).each |code| keys[code.chr.downcase] = code
# add numbers
(0x30..0x39).each |code| keys[code-0x30] = code
# add special characters
keys[';'] = 0xBA; keys['='] = 0xBB; keys[','] = 0xBC; keys['-'] = 0xBD; keys['.'] = 0xBE
keys['/'] = 0xBF; keys['`'] = 0xC0; keys['['] = 0xDB; keys[']'] = 0xDD; keys["'"] = 0xDE
keys['\\'] = 0xDC
# add custom key macros
keys["\n"] = 0x0D; keys["\t"] = 0x09; keys['(backspace)'] = 0x08; keys['(CAPSLOCK)'] = 0x14
# add for uppercase letters
('a'..'z').each |char| uppercase[char] = char.upcase
# add for uppercase numbers
uppercase[1] = '!'; uppercase[2] = '@'; uppercase[3] = '#'; uppercase[4] = '$'; uppercase[5] = '%'
uppercase[6] = '^'; uppercase[7] = '&'; uppercase[8] = '*'; uppercase[9] = '('; uppercase[0] = ')'
# add for uppercase special characters
uppercase[';'] = ':'; uppercase['='] = '+'; uppercase[','] = '<'; uppercase['-'] = '_'; uppercase['.'] = '>'
uppercase['/'] = '?'; uppercase['`'] = '~'; uppercase['['] = ''; uppercase[']'] = ''; uppercase["'"] = '"'
uppercase['\\'] = '|'
# create a listener for Windows key-presses
$listener = Win32API.new('user32', 'GetAsyncKeyState', ['i'], 'i')
# call listener once to initialize lsb's
keys.each_value |code| $listener.call(code)
logs = File.open('C://kpkt.txt', 'a')
while true
break if $listener.call(VK_ESC) != 0
keys.each do |char, code|
n = $listener.call(code)
if n and n & 0x01 == 1
check_shifts() ? logs.write("#uppercase[char]") : logs.write("#char")
end
end
end
logs.close()
【讨论】:
以上是关于在 Ruby 中检测没有 getc/gets 的按键(非阻塞)的主要内容,如果未能解决你的问题,请参考以下文章