Ruby#open 你了解多少?

共 3046字,需浏览 7分钟

 ·

2016-08-23 16:12

如果现在要你使用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为例,下图那块黑色内存块中的内容是即时输出的:

file

或者曾想过在自己的网站上执行外部的指令,并且即时呈现给使用者呢?若你有在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,反之亦然。

浏览 39
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报