简单I/O模型
简单模型虚拟了一个当前输入流和一个当前输出流,其I/O操作是通过这些流实现的。I/O库把当前输入流初始化为进程的标准输入(C语言中的stdin),将当前输出流初始化为进程的标准输出(C语言中的stdout)。因此,当执行类似于io.read()
这样的语句时,就可以从标准输入中读取一行。
函数io.input
和函数io.output
可以用于改变当前的输入输出流。调用io.input(filename)
会以只读模式打开指定文件,并将文件设置为当前输入流。之后,所有的输入都将来自该文件,除非再次调用io.input
。对于输出而言,函数io.output
的逻辑与之类似。如果出现错误,这两个函数都会抛出异常。如果想直接处理这些异常,则必须使用完整IO模型。
函数io.write
可以读取任意数量的字符串(或者数字)并将其写入当前输出流。由于调用该函数时可以使用多个参数,因此应该避免使用io.write(a..b..c)
,应该调用io.write(a, b,c)
,后者可以用更少的资源达到同样的效果,并且可以避免更多的连接动作。
作为原则,应该只在“用后即弃”的代码或调试代码中使用函数print
;当需要完全控制输出时,应该使用函数io.write
。与函数print
不同,函数io.write
不会在最终的输出结果中添加诸如制表符或换行符这样的额外内容。此外,函数io.write
允许对输出进行重定向,而函数print
只能使用标准输出。最后,函数print
可以自动为其参数调用tostring
,这一点对于调试而言非常便利,但这也容易导致一些诡异的Bugo
函数io.write
在将数值转换为字符串时遵循一般的转换规则;如果想要完全地控制这种转换,则应该使用函数string.format
io.write("sin(3) = ", math.sin(3),"In") --> 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")
可从当前位置开始读取当前输入文件的全部内容。如果当前位置处于文件的末尾或文件为空,那么该函数返回一个空字符串。
t = io.read("a") --读取整个文件
t = string.gsub(t,"bad", "good ") --进行处理
io.write(t) --输出结果
调用io.read("l")
会返回当前输入流的下一行,不包括换行符在内;调用io.read("L")
与之类似,但会保留换行符(如果文件中存在)。当到达文件末尾时,由于已经没有内容可以返回,该函数会返回nil。选项"l"是函数read的默认参数。通常只在逐行处理数据的算法中使用该参数,其他情况则更倾向于使用选项"a"一次性地读取整个文件,或者像后续介绍的按块(block)读取。
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,"in")
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) --块大小是8KB
if not block then break end
io.write(block)
end
io.read(0)
是一个特例,它常用于测试是否到达了文件末尾。如果仍然有数据可供读取,它会返回一个空字符串;否则,则返回nil。
调用函数read时可以指定多个选项,函数会根据每个参数返回相应的结果。
完整I/O模型
可以使用函数io.open
来打开一个文件,该函数仿造了C语言中的函数fopen。这个函数有两个参数,一个参数是待打开文件的文件名,另一个参数是一个模式(mode)字符串。模式字符串包括表示只读的r、表示只写的w(也可以用来删除文件中原有的内容)、表示追加的a,以及另外一个可选的表示打开二进制文件的b。函数io.open
返回对应文件的流。当发生错误时,该函数会在返回nil的同时返回一条错误信息及一个系统相关的错误码:
print(io.open( "non-existent-file","r"))
--> nilnon-existent-file: No such file or directory 2
print( io.open(" /etc/passwd" ,"w" ))
-->nil letc/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()
IO库提供了三个预定义的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.lines
从流中读取内容。返回一个可以从流中不断读取内容的迭代器。给函数io.lines
提供一个文件名,它就会以只读方式打开对应该文件的输入流,并在到达文件末尾后关闭该输入流。若调用时不带参数,函数io.lines
就从当前输入流读取。我们也可以把函数lines当作句柄的一个方法。此外,从Lua5.2开始,函数io.lines可以接收和函数io.read一样的参数。
例如,下面的代码会以在8KB为块迭代,将当前输入流中的内容复制到当前输出流中:
for block in io.input():lines(2^13) do
io.write(block)
end
其它文件操作
函数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")
会将位置重置到文件开头并返回O;调用函数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
外加一条错误信息和一个错误码。
其他系统调用
函数os.exit
用于终止程序的执行。该函数的第一个参数是可选的,表示该程序的返回状态,其值可以为一个数值(О表示执行成功)或者一个布尔值(true表示执行成功);该函数的第二个参数也是可选的,当值为true时会关闭Lua状态3并调用所有析构器释放所占用的所有内存(这种终止方式通常是非必要的,因为大多数操作系统会在进程退出时释放其占用的所有资源)。
函数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
一样,该函数运行一条系统命令,但该函数还可以重定向命令的输入/输出,从而使得程序可以向命令中写入或从命令的输出中读取。
例如,下列代码使用当前目录中的所有内容构建了一个表:
--对于POSIX系统而言,使用'ls '而非'dir'
local f = io.popen( "dir /B","r ")
local dir = {}
for entry in f:lines() do
dir[#dir + 1] = entry
end
其中,函数io.popen
的第二个参数r
表示从命令的执行结果中读取。由于该函数的默认行为就是这样,所以在上例中这个参数实际是可选的。