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:

1
2
> 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语言编写过滤器的一种简单技巧就是将整个文件读取到一个字符串中,然后对字符串进行处理,最后输出结果为:

1
2
3
t = io.read("a")			-- 读取整个文件
t = string.gsub(t,"bad","good") -- 进行处理
io.wirte(t) -- 输出结果

举一个更加具体的例子,一下是一段将某个人间的内容使用MIME可打印字符串引用编码进行编码的代码。这种编码方式将所有非ASCII字符编码为 =xx,其中xx是这个字符的十六进制。为保证编码的一致性,等号也会被编码:
1
2
3
4
5
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”一次性地读取整个文件,或者像后续介绍的按块读取。
作为面向行的输入的一个简单例子,以下的程序会在将当前输入复制到当前输出中的同时对每行进行编码:
1
2
3
4
5
6
7
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迭代器会更简单:
1
2
3
4
5
local count = 0
for line in io.lines() do
count = count + 1
io.write(string.format("%6d ",count), line , "\n")
end

另一个面向行的输入的例子参考下例,其中给出了一个对文件中的进行排序的完整程序。
1
2
3
4
5
6
7
8
9
10
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的高效方法:
1
2
3
4
5
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个数字组成的文件:
1
2
3
6.0 -3.23 15e12
4.3 234 1000001
...

如果想打印每一行的最大值,那么可以通过调用函数read来一次性地同时读取每行中的3个数字:
1
2
3
4
5
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的同时返回一条错误信息及一个系统相关的错误码:

1
2
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:
1
local f = assert(io.open(filename,mode))

如果函数io.open执行失败,错误信息会作为函数assert的第二个参数被传入,之后函数assert会将错误信息展示出来。
在打开文件后,可以使用方法read和write从流中读取和向流中写入。它们与函数read和write类似,但需要使用冒号运算符将它们当做流对象的方法来调用。例如,可以使用如下的代码打开一个文件并读取其中多有内容:
1
2
3
local f = assert(io.open(filename,"r"))
local t = f:read("a")
f:close()

I/O库提供了三个预定义的C语言流的句柄:io.stdin、io.stdout和io.stderr。例如,可以使用如下的代码将信息直接写到标准错误流中:
1
io.stderr:write(message)

函数io.input和io.output允许混用完整I/O模型和简单I/O模型。调用无参数的io.input()可以获得当前输入流,调用io.input(handle)可以设置当前输入流。例如,如果想要临时改变当前输入流,可以像这样:
1
2
3
4
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”)会将当前位置到文件结尾并返回文件的大小。下面的函数演示了如何在不修改当前位置的情况下获取文件大小:

1
2
3
4
5
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用于获取某个环境变量,该函数的输入参数是换环境变量的名称,返回值为保存了该环境变量对应值的字符串:

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

对于未定义的环境变量,该函数返回nil。

运行系统命令

函数os.execute用于运行系统命令,它等价于C语言中的函数system。该函数的参数为表示待执行命令的字符串,返回值为命令运行结束后的状态。其中,第一个返回值是一个布尔类型,当为true时表示程序成功运行完成;第二个返回值是一个字符串,当为”exit”时表示程序正常运行程序,当为”signal”时表示因信号而中断;第三个返回值是返回状态或者终结该程序的信号代码。例如,在POSIX和Windows中都可以使用如下的函数创建新目录:

1
2
3
function createDir(dirname)
os.execute("mkdir " .. dirname)
end

另一个非常有用的函数是io.popen。同函数os.execute一样,该函数运行一条系统命令,但该函数还可以重定向命令的输入/输出,从而使得程序可以向命令中写入或从命令的输出中读取。例如,下列代码使用当前目录中的所有内容构建一个表:
1
2
3
4
5
local f = io.popen("dir/B","r")
local dir = {}
for entry in f:lines() do
dir[#dir + 1] = entry
end

其中,函数io.popen的第二个参数”r”表示从命令的执行结果中读取。由于该函数的默认行为就是这样,所以在上例中这个参数实际是可选的。
下面的示例用于发送一封邮件:
1
2
3
4
5
6
7
8
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.
]])
f:close()

注意,该脚本只能在安装了相应工具包的POSIX系统中运行。上例中函数io.popen的第二个参数是”w”,表示向该命令中写入。
正如我们在上面的两个例子中看到的一样,函数os.execute和io.popen都是功能非常强大的函数,但它们也同样是非常依赖于操作系统的。
如果要使用操作系统的其他扩展功能,最好的选择是使用第三方库,比如用于基于目录操作和文件属性操作的LuaFileSystem,或者提供了POSIX.1标准支持的luaposix库。