Lua輸入輸出


    由於Lua語言強調可移植性和嵌入性,所以Lua語言本身並沒有提供太多與外部交互的機制。在真實的Lua程序中,從圖形、數據庫到網絡的網絡的訪問等大多數I/O操作,要麼遊宿主程序實現,要麼通過不包括在發行版中的外部庫實現。單就Lua語言而言,只提供IOS C語言標準支持的功能,即基本的文件操作等。

簡單I/O模型

    對於文件操作來說,I/O庫提供了兩種不同的模型。簡單模型虛擬了一個當前輸入流和一個當前輸出流,其I/O操作時通過這些流實現的。I/O庫把當前輸入流初始化爲進程的標準輸入,將當前輸出流初始化爲進程的標準輸出。因此,當執行類似於io.read()這樣的語句時,就可以從標準輸入中讀取一行。
    函數io.input可以用於改變當前的輸入輸出流。調用io.input(file-name)會以只讀模式打開指定文件,並將文件設置爲當前輸入流。之後,所有的輸入都將來自該文件,除非再次調用io.input。對於輸出流而言,函數io.output的邏輯與之類似。如果出現錯誤,這兩個函數都會拋出異常。如果想直接處理這些異常,則必須使用完整I/O模型。
    由於函數write比函數read簡單,我們首先來看函數write。函數io.write可以讀取任意數量的字符串並將其寫入當前輸入流。由於調用該函數時可以使用多個參數,因此應該避免使用io.write(a…b…c),應該調用io.write(a,b,c),後者可以用更少的資源達到同樣的效果,並且可以避免更多的連接動作。
    作爲原則,應該只在“用後即棄”的代碼或調試代碼中使用函數print;當需要完全控制輸出時,應該使用函數io.write。與函數print不同,函數io.wirte不會在最終的輸出結果中添加諸如製表符或換行符這樣的額外內容。此外,函數io.write允許對輸出進行重定向,而函數print只能使用標準輸出。最後,函數print可以自動爲其參數調用tostring,這一點對於調試而言非常便利,但這也容易導致一些詭異的Bug。
    函數io.write在將數值轉爲字符串時遵循一般的轉換規則;如果想要完全地控制這種轉換,則應該使用函數string.format:

> io.write("sin(3) = ",math.sin(3),"\n")		-- sin(3) = 0.14112000805987
> io.write(string.format("sin(3) = %0.4f\n",math.sin(3))) 			-- sin(3) = 0.1411

函數io.read可以從當前輸入流中讀取字符串,其參數決定了要讀取的數據:


“a” 讀取整個文件
“l” 讀取下一行(丟棄換行符)
“L” 讀取下一行(保留換行符)
“n” 讀取一個數值
num 以字符串讀取num個字符


    調用io.write(“a”)可以從當前位置開始讀取輸入文件的全部內容。如果當前位置處於文件的末尾或文件爲空,那麼該函數返回一個空字符串。
    因爲Lua語言可以高效地處理長字符串,所以在Lua語言編寫過濾器的一種簡單技巧就是將整個文件讀取到一個字符串中,然後對字符串進行處理,最後輸出結果爲:

t = io.read("a")			-- 讀取整個文件
t = string.gsub(t,"bad","good")			-- 進行處理
io.wirte(t)					-- 輸出結果

舉一個更加具體的例子,一下是一段將某個人間的內容使用MIME可打印字符串引用編碼進行編碼的代碼。這種編碼方式將所有非ASCII字符編碼爲 =xx,其中xx是這個字符的十六進制。爲保證編碼的一致性,等號也會被編碼:

t = io.read("all")
t = string.gsub(t,"[\128 - \255 = ]", function(c)
	return string.format("=%02X",string.byte(c))
end)
io.write(t)

函數string.gsub會匹配所有的等號及非ASCII字符(從128到255),並調用指定的函數完成替換。
    調用io.read(“l”)會返回當前輸入流的下一行,不包括換行符在內;調用io.read(“L”)與之類似,但會保留換行符。當達到文件末尾時,由於已經沒有內容可以返回,該函數會返回nil。選項"l"是函數read的默認參數。我通常只在逐行處理數據的算法使用該參數,其他情況則更傾向於使用選項"a"一次性地讀取整個文件,或者像後續介紹的按塊讀取。
    作爲面向行的輸入的一個簡單例子,以下的程序會在將當前輸入複製到當前輸出中的同時對每行進行編碼:

for count = 1 , math.huge do
	local line = io.read("L")
	if line == nil then
		break
	end
	io.write(string.format("%6d ",count),line)
end

不過,如果要逐行迭代一個文件,那麼使用io.lines迭代器會更簡單:

local count = 0
for line in io.lines() do
	count = count + 1
	io.write(string.format("%6d ",count), line , "\n")
end

另一個面向行的輸入的例子參考下例,其中給出了一個對文件中的進行排序的完整程序。

local lines = {}
for line in io.lines() do
	lines[#lines + 1] = line
end

table.sort(lines)

for _, l in ipairs(lines) do
	io.write(l,"\n")
end

    調用io.read(“n”)會從當前輸入流中讀取一個數值,這也是函數read返回值爲數值而非字符串的唯一情況。如果在跳過了空格後,函數io.read仍然不能從當前位置讀取到數值,則返回nil。
    除了上述這些基本的讀取模式外,在調用函數read時還可以用一個數字n作爲其參數:在這種情況下,函數read會從輸入流中讀取n個字符。如果無法讀取到任何字符則返回nil;否則,則返回一個由流中最多n個字符組成的字符串。作爲這種讀取模式的示例,以下的代碼展示了將文件從stdin複製到stdout的高效方法:

while true do
	local block = io.read(2^13)
	if not block then break end
	io.write(block)
end

io.read(0)是一個特例,它常用於測試是否到達了文件末尾。如果仍然有數據可供讀取,它會返回一個空字符串;否則,則返回nil。
    調用函數read時可以指定多個選項,函數會根據每個參數返回相應的結果。假設有一個每行由3個數字組成的文件:

6.0 -3.23 15e12
4.3	234	  1000001
...

如果想打印每一行的最大值,那麼可以通過調用函數read來一次性地同時讀取每行中的3個數字:

while true do 
	local n1,n2,n3 = io.read("n","n","n")
	if not n1 then break end
	print(math.max(n1,n2,n3))
end

完整I/O模型

    簡單I/O模型對簡單的需求而言還算適用,但對於諸如同時讀寫多個文件等更高級的文件操作來說就不夠了。對於這些文件操作,我們需要用到完整I/O模型。
    可以使用函數io.open來打開一個文件,該函數仿造C語言中的函數fopen。這個函數有兩個參數一個參數是待打開文件的文件名,另一個參數是一個模式字符串。模式字符串包括表示只讀的r、表示只寫的w、表示追加的a,以及另外一個可選的表示打開二進制文件的b。函數io.open返回對應文件的流。當發生錯誤時,該函數會返回nil的同時返回一條錯誤信息及一個系統相關的錯誤碼:

print(io.open("non-existent-file","r"))		-- nil  non-existent-file:No such file file or directory 2
print(io.open("/etc/passwd","w"))			-- nil  /etc/passwd:Permission denied 13

檢查錯誤的一種典型方法是使用函數assert:

local f = assert(io.open(filename,mode))

如果函數io.open執行失敗,錯誤信息會作爲函數assert的第二個參數被傳入,之後函數assert會將錯誤信息展示出來。
    在打開文件後,可以使用方法read和write從流中讀取和向流中寫入。它們與函數read和write類似,但需要使用冒號運算符將它們當做流對象的方法來調用。例如,可以使用如下的代碼打開一個文件並讀取其中多有內容:

local f = assert(io.open(filename,"r"))
local t = f:read("a")
f:close()

    I/O庫提供了三個預定義的C語言流的句柄:io.stdin、io.stdout和io.stderr。例如,可以使用如下的代碼將信息直接寫到標準錯誤流中:

io.stderr:write(message)

    函數io.input和io.output允許混用完整I/O模型和簡單I/O模型。調用無參數的io.input()可以獲得當前輸入流,調用io.input(handle)可以設置當前輸入流。例如,如果想要臨時改變當前輸入流,可以像這樣:

local temp = io.input()			-- 保存當前輸入流
io.input("newinput")			-- 打開一個新的當前輸入流
io.input():close()				-- 關閉當前流
io.input(temp)					-- 恢復此前的當前輸入流

注意,io.read(args)實際上是io.input():read(args)的簡寫,即函數read是用在當前輸入流上的。同樣,io.write(args)是io.output():write(args)的簡寫。
    除了函數io.read外,還可以用函數io.lines從流中讀取內容。正如之前的示例中展示的那樣,函數io.lines返回一個可以從流中不斷讀取內容的迭代器。給函數io.lines提供一個文件名,它就會只讀方式打開對應該文件的輸入流,並在到達文件末尾後關閉該輸入流。若調用時不帶參數,函數io.lines就從當前輸入讀取。我們也可以把函數lines當作句柄的一個方法。

其他文件操作

    函數io.tmpfile返回一個操作臨時文件的句柄,該句柄是以讀/寫模式打開的。當程序運行結束後,該臨時文件會被自動移除。
    函數flush將所有緩衝數據寫入文件。與函數write一樣,我們也可以把它當做io.flush()使用,以刷新當前輸出流;或者把它當作方法f:flush()使用,以刷新流f。
    函數setvbuf用於設置流的緩衝模式。該函數的第一個參數是一個字符串:"no"表示無緩衝,"full"表示在緩衝區滿時或者顯示地刷新文件時文件時才寫入數據,“line"表示輸出一直被緩衝直到遇到換行符或從一些特定文件中讀取到了數據。對於後兩個選項,函數setvbuf支持可選的第二個參數,用於指定緩衝區大小。
    在大多數系統中,標準錯誤流(io.stdrr)是不被緩衝的,而標準輸出流(io.stdout)按行緩衝。因此,當向標準輸出中寫入了不完整的行時,可能需要刷新這個輸出流才能看到輸出結果。
    函數seek用來獲取和設置文件的當前位置,常常使用f:seek(whence,offset)的形式來調用,其中參數whence是一個指定如何使用偏移的字符串。當參數whence取值爲”set“時,表示相對文件開頭的偏移;取值爲"cur"時,表示相對於文件位置的偏移;取值爲"end"時,表示相對於文件尾部的偏移。不管whence的取值是什麼,該函數都會以字節爲單位,返回當前新位置在流中的相對於文件開頭的偏移。
    whence的默認值是"cur”,offset的默認值是0。因此,調用函數file:seek()會返回當前的位置且不改變當前位置;調用函數file:seek(“set”)會將位置重置到文件開頭並返回0;調用函數file:seek(“end”)會將當前位置到文件結尾並返回文件的大小。下面的函數演示瞭如何在不修改當前位置的情況下獲取文件大小:

function fsize (file)
	local current = file:seek()				-- 保存當前位置
	local size = file:seek("end")			-- 獲取文件大小
	file:seek("set",current)				-- 恢復當前位置
end

    此外,函數os.rename用於文件重命名,函數os.remove用於移除(刪除)文件。需要注意的是,由於這兩個函數處理的是真實文件而非流,所以它們位於os庫而非io庫中。
    上述所有的函數在遇到錯誤時,均會返回nil外加一條錯誤信息和一個錯誤新。

其他系統調用

    函數os.exit用於終止程序的執行。該函數的第一個參數是可選的,表示該程序的返回狀態,其值可以爲一個數值(0表示執行成功)或者一個布爾值(true表示執行成功);該函數的第二個參數也是可選的,當值爲true時會關閉Lua狀態並調用所有析構器釋放所用的所有內存。
    函數os.getenv用於獲取某個環境變量,該函數的輸入參數是換環境變量的名稱,返回值爲保存了該環境變量對應值的字符串:

print(os.getenv("HOME")) 	-- /home/lua

對於未定義的環境變量,該函數返回nil。

運行系統命令

    函數os.execute用於運行系統命令,它等價於C語言中的函數system。該函數的參數爲表示待執行命令的字符串,返回值爲命令運行結束後的狀態。其中,第一個返回值是一個布爾類型,當爲true時表示程序成功運行完成;第二個返回值是一個字符串,當爲"exit"時表示程序正常運行程序,當爲"signal"時表示因信號而中斷;第三個返回值是返回狀態或者終結該程序的信號代碼。例如,在POSIX和Windows中都可以使用如下的函數創建新目錄:

function createDir(dirname)
	os.execute("mkdir " .. dirname)
end

    另一個非常有用的函數是io.popen。同函數os.execute一樣,該函數運行一條系統命令,但該函數還可以重定向命令的輸入/輸出,從而使得程序可以向命令中寫入或從命令的輸出中讀取。例如,下列代碼使用當前目錄中的所有內容構建一個表:

local f = io.popen("dir/B","r")
local dir = {}
for entry in f:lines() do
	dir[#dir + 1] = entry
end

其中,函數io.popen的第二個參數"r"表示從命令的執行結果中讀取。由於該函數的默認行爲就是這樣,所以在上例中這個參數實際是可選的。
    下面的示例用於發送一封郵件:

local subject = "some news"
local address = "[email protected]"
local cmd = string.format("mail -s '%s' '%s' ",subject,address)
local f = io.popen(cmd,"w")
f:write([[
	Nothing important to say.
	]])
f:close()

注意,該腳本只能在安裝了相應工具包的POSIX系統中運行。上例中函數io.popen的第二個參數是"w",表示向該命令中寫入。
    正如我們在上面的兩個例子中看到的一樣,函數os.execute和io.popen都是功能非常強大的函數,但它們也同樣是非常依賴於操作系統的。
    如果要使用操作系統的其他擴展功能,最好的選擇是使用第三方庫,比如用於基於目錄操作和文件屬性操作的LuaFileSystem,或者提供了POSIX.1標準支持的luaposix庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章