Ruby#open 你了解多少?
如果現(xiàn)在要你使用Ruby,你會(huì)想到怎么做?
直覺是使用File.open,但想想File.new似乎也可行,然后又發(fā)現(xiàn)不使用File類別,直接用open也能做到一樣的事。去查了Ruby文件結(jié)果發(fā)現(xiàn)IO.open和IO.new也能做到同樣的操作。
如你所見,使用Ruby光是開個(gè)檔案描述符(以下簡(jiǎn)稱FD)就有數(shù)幾種方法,令人眼花撩亂,常看到的是有人用同一招打天下,卻一直沒有去了解其他的方法與其是用情境,有些可惜,而這篇文章將通過由下而上的方式,一一介紹、示范它們的差別和使用。
IO.new
IO類別是Ruby對(duì)FD進(jìn)行讀寫操作的一切基礎(chǔ),我們可以用File來(lái)操作是因?yàn)镕ile繼承自IO,只是稍嫌麻煩些。
IO.new的第一個(gè)參數(shù)必須是FD,或在Windows下則稱句柄,無(wú)論何者都只是一個(gè)數(shù)字。
如果你已知標(biāo)準(zhǔn)輸入與標(biāo)準(zhǔn)輸出的檔案描述符分別為0和1,不妨實(shí)驗(yàn)一下:
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來(lái)取得檔案的FD,這其實(shí)就是File類別的做法,F(xiàn)ile只是隱藏此細(xì)節(jié)罷了:
fd = IO.sysopen('file.txt','w')#=> 3
io = IO.new(fd)
io.puts 'hello!'
io.close
另一個(gè)例子是通過/dev/tty寫到終端:
fd = IO.sysopen('/dev/tty','w')
io = IO.new(fd,'w')
puts 'Hello'
io.puts 'World'
io.close
Hello World
在這里提醒要小心選擇正確的tty檔案,萬(wàn)一不慎選到其他使用者的,執(zhí)行上述代碼就會(huì)在他人的終端畫面上印出一堆垃圾。
IO.open
IO.open沒什么新奇之處,它只是IO.new加上block的擴(kuò)充版本,若無(wú)使用block時(shí),與IO.new無(wú)異,最后會(huì)回傳IO物件;但若與block使用,有兩個(gè)特點(diǎn):
IO物件會(huì)在block結(jié)束時(shí)被自動(dòng)關(guān)閉(意即不需要寫IO#close)。
IO.open最后回傳的不再是IO物件,而是block的最后執(zhí)行結(jié)果。
IO.popen
有曾好奇過市面上的CI是怎么做到即時(shí)顯示終端上的文字嗎?以Travis CI為例,下圖那塊黑色內(nèi)存塊中的內(nèi)容是即時(shí)輸出的:

或者曾想過在自己的網(wǎng)站上執(zhí)行外部的指令,并且即時(shí)呈現(xiàn)給使用者呢?若你有在Ruby中呼叫其他系統(tǒng)指令的經(jīng)驗(yàn)(例如ls、cat、bundle install等等),那應(yīng)該對(duì)system、%x{}或是``不陌生:
system 'date' # => true,false or nil
%x{date} # => the standard output of the running cmd
date # => as above
然而system只根據(jù)指令執(zhí)行結(jié)果成功與否回傳布爾值,無(wú)法直接存取子程序輸出的結(jié)果;%x{}會(huì)以字串形式回傳結(jié)果,但必須等到子程序執(zhí)行結(jié)束后才會(huì)回傳整個(gè)字串,無(wú)法即時(shí)監(jiān)控子程序的標(biāo)準(zhǔn)輸出。
相較于%x{}回傳完整的字串,IO.popen則是回傳IO物件。為了比較出差異,這里就拿ping指令為例,因?yàn)樵撝噶顣?huì)不斷在終端畫面上輸出信息,直到使用者手動(dòng)停止,如果使用%x{}的話,Ruby程序?qū)?huì)卡在該處,且因準(zhǔn)備要回傳的字串越來(lái)越長(zhǎng),最后導(dǎo)致內(nèi)存不夠用或程序會(huì)卡到海枯石爛。
相較下操作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 …
當(dāng)然這還離真正做出一個(gè)在網(wǎng)頁(yè)上呈現(xiàn)終端執(zhí)行畫面的功能還很遠(yuǎn),例如上述的代碼卡在一個(gè)無(wú)窮循環(huán)里面,你可能會(huì)想針對(duì)IO阻塞問題做出一些改善,像是配合IO.select或是IO#read_nonblock等,但純屬延伸議題,不在本章范圍,有機(jī)會(huì)筆者會(huì)在另一篇章中分享怎么做到:)
File.new與File.open
這兩個(gè)方方法就是大家耳熟能詳?shù)拈_檔方案了,它們和IO.new與IO.open幾乎一樣,只差在復(fù)寫了initialize方法,使其接受的參數(shù)不再是FD而是檔案的路徑字串。File.new回傳值也和IO.new一樣是IO物件;在File.open與block同時(shí)使用的情況下也和IO.open一樣,會(huì)自動(dòng)關(guān)檔,且回傳block的最后執(zhí)行結(jié)果。
Kernel.open
Kernel.open大概是最萬(wàn)用的方法了,留在最后講是因?yàn)樗荌O.popen與File.open的合體,除此也接受擁有#to_open方法的物件。
當(dāng)傳入一個(gè)物件給Kernel.open時(shí),處理的優(yōu)先續(xù)如下:
檢查該物件是否有#to_open方法,有則直接呼叫以取得IO物件。
如果物件是字串且開頭是|,則去掉|,剩下丟給IO.popen處理。
最后交給File.open處理
to_open
關(guān)于#to_open Ruby文件上沒有一處提及,只記載在Ruby原始碼中。實(shí)作的時(shí)候必要回傳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
該用哪個(gè)?
這沒有什么強(qiáng)制的規(guī)范,畢竟Ruby是一個(gè)自由的程序語(yǔ)言,比較接近Perl,和一板一眼的Python不太一樣(Only one way to do it)。不過建議大原則是盡量使用易讀易寫的API來(lái)完成工作,如果有細(xì)節(jié)需要處理再用其他的方法。例如一般開檔就使用File.open或是Kernel.open即可,需要存取FD則改用IO.open,若要手動(dòng)關(guān)檔再考慮File.new或IO.new。另外也不要特別使用Kernel.open調(diào)用IO.popen的奇怪語(yǔ)法(|),這會(huì)降低代碼的可讀性,不符合易讀易寫。像IO.popen('date')就比Kernel.open('|date')好懂多了。
另一個(gè)原則是代碼的一致性,如果團(tuán)隊(duì)開檔案都使用File.open,那就盡量避免特立獨(dú)行使用Kernel.open,反之亦然。
