????????由于 Lua 語言強調可移植性和嵌入性 , 所以 Lua 語言本身并沒有提供太多與外部交互的機制 。 在真實的 Lua 程序中,從圖形、數據庫到網絡的訪問等大多數 I/O 操作,要么由宿主程序實現,要么通過不包括在發行版中的外部庫實現。 單就 Lua 語言而言,只提供了 ISO C語言標準支持的功能, 即基本的文件操作等。 在這一章中,我們將會學習標準庫如何支持這些功能。
7.1 ?簡單 I/O 模型
????????對于文件操作來說, I/O 庫提供了兩種不同的模型 。 簡單模型虛擬了一個當前輸入流和一個當前輸出流,其 I/O 操作是通過這些流實現的 。 I/O 庫把當前輸入流初始化為進程的標準輸入( C 語言中的 stdin ),將當前輸出流初始化為進程的標準輸出( C 語言中的 stdout ) 。 因此 ,當執行類似于 io.read()?這樣的語句時,就可以從標準輸入中讀取一行。
????????函數 io.input 和函數 io.output 可以用于改變當前的輸入輸出流。 調用 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.write 不會在最終的輸出結果中添加諸如制表符或換行符這樣的額外內容。 此外,函數 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) = %.4f\n", math.sin(3)))
sin(3) = 0.1411
????????函數 io.read 可以從當前輸入流中讀取字符串,其參數決定了要讀取的數據:
"a" | 讀取整個文件 |
"l" | 讀取下一行(丟棄換行符) |
"L" | 讀取下一行(保留換行符) |
"n" | 讀取一個數值 |
num | 以字符串讀取num個字符 |
????????調用 io.read("a") 可從當前位置開始讀取當前輸入文件的全部內容。如果當前位置處于文件的末尾或文件為空,那么該函數返回一個空字符串 。
????????因為 Lua 語言可以高效地處理長字符串,所以在 Lua 語言中編寫過濾器( filter )的一種簡單技巧就是將整個文件讀取到一個字符串中 , 然后對字符串進行處理,最后輸出結果為:
t = io.read("a") -- 讀取整個文件
t = string.gsub(t, "bad", "good") -- 進行處理
io.write(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 dolocal line = io.read("L")if line == nil then break endio.write(string.format("%6d ", count), line)
end
不過,如果要逐行迭代一個文件,那么使用 io.lines 迭代器會更簡單:
local count = 0
for line in io.lines() docount = count + 1 io.write(string.format("%6d ", count), line, "\n")
end
另一個面向行的輸入的例子參見示例 7.1 ,其中給出了一個對文件中的行進行排序的完整程序。
示例 7.1 對文件進行排序的程序
local lines = {}-- 將所有行讀取到表"lines"中
for line in io.lines() dolines[#lines + 1] = line
end-- 排序
table.sort(lines)-- 輸出所有的行
for _, l in ipairs(lines) doio.write(l, "\n")
end
????????調用 io.read ("n") 會從當前輸入流中讀取一個數值,這也是函數 read 返回值為數值(整型或者浮點型,與 Lua 語法掃描器的規則一致) 而非字符串的唯一情況。 如果在跳過了空格后,函數 io.read 仍然不能從當前位置讀取到數值(由于錯誤的格式問題或到了文件末尾),則返回 nil。
????????除了上述這些基本的讀取模式外,在調用函數 read 時還可以用一個數字 n 作為其參數 :在這種情況下,函數 read 會從輸入流中讀取 n 個字符。 如果無法讀取到任何字符(處于文件末尾)則返回 nil ;否則,則返回一個由流中最多 n 個字符組成的字符串 。 作為這種讀取模式的示例,以下的代碼展示了將文件從 stdin 復制到 stdout 的高效方法:
while true do local block = io.read(2^13) -- 塊大小是8KBif not block then break endio.write(block)
end
????????io.read(0) 是一個特例,它常用于測試是否到達了文件末尾 。 如果仍然有數據可供讀取,它會返回一個空字符串;否則,則返回 nil 。
????????調用函數 read 時可以指定多個選項,函數會根據每個參數返回相應的結果。 假設有一個每行由 3 個數字組成的文件:
如果想打印每一行的最大值,那么可以通過調用函數 read 來一次性地同時讀取每行中的3個數字:
6.1 -3.23 15e12
4.3 234 1000001
... while true dolocal n1, n2, n3 = io.read("n", "n", "n")if not n1 then break endprint(math.max(n1,n2,n3))
end
7.2 ?完整 I/O 模型
????????簡單 I/O 模型對簡單的需求而言還算適用,但對于諸如同時讀寫多個文件等更高級的文件操作來說就不夠了 。 對于這些文件操作,我們需要用到完整 I/O 模型 。
????????可以使用函數 io.open 來打開一個文件,該函數仿造了 C 語言中的函數 fopen 。 這個函數有兩個參數,一個參數是待打開文件的文件名,另一個參數是一個模式 ( mode )字符串 。模式字符串包括表示只讀的 r、 表示只寫的 w (也可以用來刪除文件中原有的內容)、表示追加的 a,?以及另外一個可選的表示打開二進制文件的 b 。 函數 io.open 返回對應文件的流。 當發生錯誤時,該函數會在返回 nil 的同時返回一條錯誤信息及一個系統相關的錯誤碼 :
> print(io.open("non-existent-file", "r"))
nil non-existent-file: No such file or directory 2
>
> print(io.open("/etcpasswd", "w"))
nil /etcpasswd: 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 )可以設置當前輸入流(類似的調用同樣適用于函數 io.output ) 。 例如,如果想要臨時改變當前輸入流,可以像這樣:
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當作句柄的一個方法。 此外,從 Lua 5.2 開始 函數 io.lines 可以接收和函數 io.read 一樣的參數。 例如,下面的代碼會以在 8KB為塊迭代,將當前輸入流中的內容復制到當前輸出流中:
for block in io.input():lines(2^13) doio.write(block)
end
7.3 ?其他文件操作
????????函數 io.tmpfile 返回一個操作臨時文件的句柄,該句柄是以讀/寫模式打開的 。 當程序運行結束后,該臨時文件會被自動移除(刪除)。
????????函數 flush 將所有緩沖數據寫入文件。 與函數 write 一樣,我們也可以把它當作 io.flush()使用 ,以刷新當前輸出流;或者把它當作方法 f:flush() 使用,以刷新流 f 。
????????函數 setvbuf 用于設置流的緩沖模式。該函數的第一個參數是一個字符串:"no" 表示無緩沖,” full ” 表示在緩沖區滿時或者顯式地刷新文件時才寫入數據, "line"表示輸出一直被緩沖直到遇到換行符或從一些特定文件(例如終端設備)中讀取到了數據。 對于后兩個選項,函數 setvbuf 支持可選的第二個參數,用于指定緩沖區大小。在大多數系統中,標準錯誤流( io.stderr)是不被緩沖的, 而標準輸出流(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) -- 恢復當前位置return size
end
????????此外,函數 os.rename 用于文件重命名,函數 os.remove 用于移除(刪除)文件。 需要注意的是,由于這兩個函數處理的是真實文件而非流,所以它們位于 os 庫而非 io 庫中 。
????????上述所有的函數在遇到錯誤時,均會返回 nil 外加一條錯誤信息和一個錯誤碼。
7.4 ?其他系統調用
????????函數 os.exit 用于終止程序的執行。 該函數的第一個參數是可選的,表示該程序的返回狀態,其值可以為一個數值( 0 表示執行成功)或者一個布爾值( true 表示執行成功);該函數的第二個參數也是可選的,當值為 true 時會關閉 Lua 狀態,并調用所有析構器釋放所占用的所有內存(這種終止方式通常是非必要的,因為大多數操作系統會在進程退出時釋放其占用的所有資源)。
????????函數 os.getenv 用于獲取某個環境變量,該函數的輸入參數是環境變量的名稱,返回值為保存了該環境變量對應值的字符串:
print(os.getenv("HOME")) -- /home/lua
? ? ? ? 對于未定義的環境變量,該函數返回 nil 。
7.4.1 ?運行系統命令
????????函數 os.execute 用于運行系統命令,它等價于 C 語言中的函數 system 。 該函數的參數為表示待執行命令的字符串,返回值為命令運行結束后的狀態。 其中,第一個返回值是一個布爾類型,當為 true 時表示程序成功運行完成;第二個返回值是一個字符串,當為 ” exit ”時表示程序正常運行結束,當為 “signal ”時表示因信號而中斷 ; 第三個返回值是返回狀態(若該程序正常終結)或者終結該程序的信號代碼。 例如,在 POSIX 和 Windows 中都可以使用如下的函數創建新目錄 :
function createDir(dirname)os.execute("mkdir " .. dirname)
end
????????另一個非常有用的函數是 io.popen 。 同函數 os.execute 一樣,該函數運行一條系統命令,但該函數還可以重定向命令的輸入/輸出,從而使得程序可以向命令中寫入或從命令的輸出中讀取。 例如,下列代碼使用當前目錄中的所有內容構建了一個表:
-- 對于POSIX系統而言,使用'ls'而非'dir'
local f = io.popen("dir /B", "r")
local dir = {}
for entry in f:lines() dodir[#dir + 1] = entry
end
其中 ,函數 io.popen 的第二個參數”r” 表示從命令的執行結果中讀取。 由于該函數的默認行為就是這樣,所以在上例中這個參數實際是可選的 。
????????下面的示例用于發送一封郵件:
local subject = "some news"
local address = "someone@somewhere.org"local cmd = string.format("mail -s '%s' '%s'", subject, address)
local f = io.popen(cmd, "w")
f:write([[ Nothing important to say. -- me ]])
f:close()
注意 , 該腳本只能在安裝了相應工具包的 POSIX 系統中運行。 上例中函數 io.popen 的第二個參數是” w ”, 表示向該命令中寫入。
????????正如我們在上面的兩個例子中看到的一樣,函數 os.execute 和 io.popen 都是功能非常強大的函數,但它們也同樣是非常依賴于操作系統的 。
????????如果要使用操作系統的其他擴展功能,最好的選擇是使用第三方庫, 比如用于基本目錄操作和文件屬性操作的 LuaFileSystem ,或者提供了 POSIX.1標準支持的 luaposix 庫。