06-输入输出

争取两三周入门Lua!

由于Lua强调可移植性和嵌入性,所以Lua语言本身并没有提供太多与外部交互的机制。

简单I/O模型

简单I/O模型虚拟了一个当前输入流和一个当前输出流,其I/O操作是通过这些流实现的。I/O库把当前输入流初始化为进程的标准输入(C语言中的stdin),将当前输出流初始化为进程的标准输出(C语言的stdout)。

改变当前输入/输出流

调用函数io.input(filename) 会以只读模式打开指定文件,并将文件设置为当前输入流,之后所有的输入都将来自该文件。同理,调用函数io.output(filename)会将指定文件设置为当前输出流。

如果出现错误,这两个函数会抛出异常。如果想直接处理这些异常,得使用下面提到的完整I/O模型。

读写操作

写操作

函数io.write可以读取任意数量的字符串/数字并将其写入当前输出流。由于调用该函数时可以使用多个参数,因此应避免使用io.write(a..b..c),而是使用io.write(a, b, c),后者可以用更少的资源达到同样的效果,并且可以避免更多的连接动作。

和输出函数print不同的是,函数io.write 不会在最终的输出结果中添加诸如制表符或换行符这样的额外内容;而且print只能用标准输出stdout,而io.write则可以重定向输出。

函数io.write在将数值转换为字符串时遵循一般的转换规则:

io.write("sin(3) = ", math.sin(3), "\n")	--> sin(3) = 0.14112000805987

如果想要完全控制这种转换,需要用函数string.format

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

读操作

函数io.read可以从当前输入流中读取字符串,其参数决定了要读取的数据:

参数要读取的数据
"a"读取整个文件
"l"读取下一行(丢弃换行符),是io.read的默认参数
"L"读取下一行(保留换行符)
"n"读取一个数值
num以字符串形式读取num个字符,也被称为“块读取”。
其中,io.read(0)常用于测试是否到达EOF,是的话返回nil,否则返回空串

例如,以下代码段将某文件内容的所有非ASCII字符用MIME编码:

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

io.lines()迭代器可以逐行读取一个文件:

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

像其他函数一样,io.read()也支持多返回值,例如要一下子读取3个数值可以这样写:

local n1, n2, n3 = io.read("n", "n", "n")

完整I/O模型

如果要执行同时读写多个文件等高级操作,简单I/O模型就力不从心了。还需要完整I/O模型。

打开文件

可以用io.open()打开一个文件,该函数类似于C语言中的fopen()

这个函数有两个参数:

  • 要打开的文件名
  • 模式字符串,如 表示只读的r;表示只写的w(也可用来删除文件中原有的内容);表示追加的a;表示打开二进制文件的b

这个函数的返回值是对应文件的“流”,如果打开文件时发生错误会返回nil、错误信息和错误码。

# 打开不存在的文件
io.open("notExist.txt", "r")
nil     notExist.txt: No such file or directory 2

可以用assert()来检查错误,该函数接收两个参数,第一个是表达式,第二个是错误信息,如果表达式结果为nil,那么会输出错误信息:

assert(io.open("notExist.txt", "r")) 

stdin:1: notExist.txt: No such file or directory
stack traceback:
        [C]: in function 'assert'
        stdin:1: in main chunk
        [C]: in ?

打开文件后,可以用它的方法readwrite对流进行读写操作:

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

预定义的输入/输出流

I/O库提供了三个预定义的C语言流的句柄:io.stdinio.stdoutio.stderr。例如,可以使用如下代码将信息直接写入标准错误流中:

io.stderr:write(message)

调用无参数的io.input()可以获得当前输入流,调用io.input(handle)可以设置当前输入流,io.output()同理。实际上,io.read(args)io.input():read(args)的缩写,io.write(args)同理。

使用io.lines()也能以行读取当前流中的内容,前提是没有参数。从Lua5.2开始,io.lines()可以接收和io.read()一样的参数。

其他文件操作

操作临时文件

函数io.tmpfile()返回一个操作临时文件的句柄,该句柄是以读/写模式打开的。当程序运行结束后,该临时文件会被自动删除。

流操作

刷新流

成员函数flush()将所有缓冲数据写入文件。例如io.flush()刷新当前输出流;f:flush()刷新流f

设置流的缓冲模式

成员函数setvbuf()用于设置流的缓冲模式,该函数的参数如下:

  • 第一个参数指定模式:"no"表示无缓冲;"full"表示缓冲区满/显式调用flush()时才写入数据;"line"表示输出一直被缓冲直至遇到换行符或从一些特定文件(例如终端设备)中读取到了数据。
  • 第二个参数是可选的,指定缓冲区大小。

在大多数系统中,标准错误流io.stderr是不被缓冲的,而标准输出流io.stdout按行缓冲。因此,当向标准输出中写入了不完整的行(例如进度条时),可能需要刷新这个输出流才能看到输出结果。

设置读取文件的位置

函数seek用来获取和设置文件的当前位置,常常使用f:seek(whence, offset)的形式来调用,其中:

  • 参数whence指定偏移模式:"set"表示相对于文件开头的偏移;"cur"表示相对于文件当前位置的偏移;"end"表示相对于文件尾部的偏移。
  • 参数offset指定偏移量,单位为字节。

它的默认值为f:seek("cur", 0),因此调用f:seek()会返回当前的位置且不改变;调用f:seek("set")会将位置重置到文件开头并返回0;调用f:seek("end")会将当前位置重置到文件结尾并返回文件的大小。

可以用seek()在不修改当前读入位置的情况下获得文件大小:

function fsize(file)
    local current = file:seek()		-- 保存当前位置
    local size = file:seek("end")	-- 获取文件大小
    file:seek("cur", current)		-- 恢复当前位置
	return size
end

文件重命名

函数os.remane()用于文件重命名。

删除文件

函数os.remove()用于删除文件。

其他系统调用

终止程序执行

函数os.exit()用于终止程序的执行,有两个可选参数:

  • 第一个参数是该程序的返回状态,执行成功的值为0true
  • 第二个参数是退出程序后是否要析构所有资源;

获取环境变量

函数os.getenv()用于获取某个环境变量:

print(os.getenv("HOME")) 		--> nil
print(os.getenv("JAVA_HOME")) 	--> D:\JavaJDK

运行系统命令

使用系统调用

函数os.execute()用于运行系统命令,类似于C语言中的system()。该函数的参数为一个表示要执行命令的字符串;返回值有三个:

  • 第一个返回值是一个布尔类型,表示命令是否成功执行;
  • 第二个返回值是一个字符串,当为"exit"时表示命令正常执行结束,为"signal"时表示因信号而中断;
  • 第三个返回值是返回状态(该程序正常终结)或终结该程序的信号代码。

使用io库函数

函数io.popen()也能运行系统命令,但该函数 还可以重定向命令的输入/输出,从而使得程序可以向命令中写入或从命令输出中读取,返回值就是IO流,可以用io.lines()对命令的执行结果按行遍历。