Ruby#open 你了解多少?
如果现在要你使用Ruby,你会想到怎么做?
直觉是使用File.open,但想想File.new似乎也可行,然后又发现不使用File类别,直接用open也能做到一样的事。去查了Ruby文件结果发现IO.open和IO.new也能做到同样的操作。
如你所见,使用Ruby光是开个档案描述符(以下简称FD)就有数几种方法,令人眼花撩乱,常看到的是有人用同一招打天下,却一直没有去了解其他的方法与其是用情境,有些可惜,而这篇文章将通过由下而上的方式,一一介绍、示范它们的差别和使用。
IO.new
IO类别是Ruby对FD进行读写操作的一切基础,我们可以用File来操作是因为File继承自IO,只是稍嫌麻烦些。
IO.new的第一个参数必须是FD,或在Windows下则称句柄,无论何者都只是一个数字。
如果你已知标准输入与标准输出的档案描述符分别为0和1,不妨实验一下:
stdin = IO.new(0)
stdout = IO.new(1)
stdout.puts“what's your name?”
name = stdin.gets.chomp!
stdout.puts“hello,#{name}!”
what's your name? tony hello,tony!
另外可用IO.sysopen来取得档案的FD,这其实就是File类别的做法,File只是隐藏此细节罢了:
fd = IO.sysopen('file.txt','w')#=> 3
io = IO.new(fd)
io.puts 'hello!'
io.close
另一个例子是通过/dev/tty写到终端:
fd = IO.sysopen('/dev/tty','w')
io = IO.new(fd,'w')
puts 'Hello'
io.puts 'World'
io.close
Hello World
在这里提醒要小心选择正确的tty档案,万一不慎选到其他使用者的,执行上述代码就会在他人的终端画面上印出一堆垃圾。
IO.open
IO.open没什么新奇之处,它只是IO.new加上block的扩充版本,若无使用block时,与IO.new无异,最后会回传IO物件;但若与block使用,有两个特点:
IO物件会在block结束时被自动关闭(意即不需要写IO#close)。
IO.open最后回传的不再是IO物件,而是block的最后执行结果。
IO.popen
有曾好奇过市面上的CI是怎么做到即时显示终端上的文字吗?以Travis CI为例,下图那块黑色内存块中的内容是即时输出的:
或者曾想过在自己的网站上执行外部的指令,并且即时呈现给使用者呢?若你有在Ruby中呼叫其他系统指令的经验(例如ls、cat、bundle install等等),那应该对system、%x{}或是``不陌生:
system 'date' # => true,false or nil
%x{date} # => the standard output of the running cmd
date
# => as above
然而system只根据指令执行结果成功与否回传布尔值,无法直接存取子程序输出的结果;%x{}会以字串形式回传结果,但必须等到子程序执行结束后才会回传整个字串,无法即时监控子程序的标准输出。
相较于%x{}回传完整的字串,IO.popen则是回传IO物件。为了比较出差异,这里就拿ping指令为例,因为该指令会不断在终端画面上输出信息,直到使用者手动停止,如果使用%x{}的话,Ruby程序将会卡在该处,且因准备要回传的字串越来越长,最后导致內存不够用或程序会卡到海枯石烂。
相较下操作IO物件就可以一次读一行:
puts %x{ping www.alphacamp.co} # don't do this
io = IO.popen('ping www.alphacamp.co')
while line = io.gets
print line
end
PING www.alphacamp.co(198.41.206.122):56 data bytes 64 bytes from 198.41.206.122: icmp_seq=0 ttl=58 time=2.794 ms 64 bytes from 198.41.206.122: icmp_seq=1 ttl=58 time=4.876 ms 64 bytes from 198.41.206.122: icmp_seq=2 ttl=58 time=7.081 ms …
当然这还离真正做出一个在网页上呈现终端执行画面的功能还很远,例如上述的代码卡在一个无穷循环里面,你可能会想针对IO阻塞问题做出一些改善,像是配合IO.select或是IO#read_nonblock等,但纯属延伸议题,不在本章范围,有机会笔者会在另一篇章中分享怎么做到:)
File.new与File.open
这两个方方法就是大家耳熟能详的开档方案了,它们和IO.new与IO.open几乎一样,只差在复写了initialize方法,使其接受的参数不再是FD而是档案的路径字串。File.new回传值也和IO.new一样是IO物件;在File.open与block同时使用的情况下也和IO.open一样,会自动关档,且回传block的最后执行结果。
Kernel.open
Kernel.open大概是最万用的方法了,留在最后讲是因为它是IO.popen与File.open的合体,除此也接受拥有#to_open方法的物件。
当传入一个物件给Kernel.open时,处理的优先续如下:
检查该物件是否有#to_open方法,有则直接呼叫以取得IO物件。
如果物件是字串且开头是|,则去掉|,剩下丢给IO.popen处理。
最后交给File.open处理
to_open
关于#to_open Ruby文件上没有一处提及,只记载在Ruby原始码中。实作的时候必要回传IO物件即可:
class Foo
def to_open
puts 'Foo#to_open is here'
File.open('test.txt')#=> IO instance
end
end
open Foo.new do |io|
… io will be closed automatically
end
该用哪个?
这没有什么强制的规范,毕竟Ruby是一个自由的程序语言,比较接近Perl,和一板一眼的Python不太一样(Only one way to do it)。不过建议大原则是尽量使用易读易写的API来完成工作,如果有细节需要处理再用其他的方法。例如一般开档就使用File.open或是Kernel.open即可,需要存取FD则改用IO.open,若要手动关档再考虑File.new或IO.new。另外也不要特别使用Kernel.open调用IO.popen的奇怪语法(|),这会降低代码的可读性,不符合易读易写。像IO.popen('date')就比Kernel.open('|date')好懂多了。
另一个原则是代码的一致性,如果团队开档案都使用File.open,那就尽量避免特立独行使用Kernel.open,反之亦然。