简单学一下Lua这个有点过时的轻量级脚本语言吧,因为很多工具(nvim、xmake、MySQL等)都采用了Lua脚本提供配置, 而且直到现在,在c++项目使用Lua脚本提供配置也是一个可以考虑的方案。

关于Lua的教程很多都是速成版的,因为内容实在比较简单,比如Learn Lua in Y minutes

编译安装

Lua是一种开源的脚本语言,完全使用C语言编写,Lua官网直接提供了源码, 在Linux系统中的下载和源码编译流程如下

1
2
3
4
curl -L -R -O https://www.lua.org/ftp/lua-5.4.7.tar.gz
tar zxf lua-5.4.7.tar.gz
cd lua-5.4.7
make all test

编译命令非常简单,编译完成后可以得到两个产物:lualuac,两者仍然存放在src/文件夹中,将其移动到其它位置, 然后将路径添加到环境变量即可。其实编译产物中还有一个Lua库,用于嵌入到C语言项目中,但是暂时不需要。

由于Lua的源码编译过程本身非常简单,我们可以迁移到Windows上进行源码编译,为了方便还可以把Makefile改成CMakeLists.txt, 为了简化直接删除了安装和卸载的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
cmake_minimum_required(VERSION 3.10)
project(lua LANGUAGES C)

# Set output directories
# ./bin
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})

# ./lib
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/lib")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY})
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY})

# ./lib
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/lib")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${CMAKE_LIBRARY_OUTPUT_DIRECTORY})

# Lua version
set(LUA_VERSION 5.4)
set(LUA_RELEASE 5.4.6)

# Source files
set(SOURCE_FILES
src/lapi.c
src/lauxlib.c
src/lbaselib.c
src/lcode.c
src/lcorolib.c
src/lctype.c
src/ldblib.c
src/ldebug.c
src/ldo.c
src/ldump.c
src/lfunc.c
src/lgc.c
src/linit.c
src/liolib.c
src/llex.c
src/lmathlib.c
src/lmem.c
src/loadlib.c
src/lobject.c
src/lopcodes.c
src/loslib.c
src/lparser.c
src/lstate.c
src/lstring.c
src/lstrlib.c
src/ltable.c
src/ltablib.c
src/ltm.c
src/lundump.c
src/lutf8lib.c
src/lvm.c
src/lzio.c
)

# Header files
set(HEADER_FILES
src/lua.h
src/luaconf.h
src/lualib.h
src/lauxlib.h
src/lua.hpp
)

# Add library
add_library(lualibrary STATIC ${SOURCE_FILES})
target_include_directories(lualibrary PUBLIC src)


# Add executables
add_executable(lua src/lua.c)
target_link_libraries(lua lualibrary)

add_executable(luac src/luac.c)
target_link_libraries(luac lualibrary)

# Group the executables
set(EXEC_FILES
lua
luac
)

# Convenience target to build all
add_custom_target(build_all
DEPENDS ${EXEC_FILES}
)

# Custom target to clean build files
add_custom_target(clean_all
COMMAND ${CMAKE_COMMAND} --build . --target clean
)

message("Configuration done for Lua ${LUA_VERSION}")

基本使用

Lua是一种解释型的脚本语言,解释器lua既可以交互式使用,也可以执行整个脚本文件(习惯使用.lua作为文件后缀), 还可以预先使用编译器luac将脚本编译为不可读的字节码文件(习惯使用.luac作为文件后缀,但默认名为luac.out), 解释器可以更高效地执行字节码文件,无需提供原始的脚本文件。

示例如下

1
2
3
4
5
6
7
8
9
# (1)
lua

# (2)
lua hello.lua

# (3)
luac -o hello.luac hello.lua
lua hello.luac

注意:

  • lua解释器的使用方式优点奇怪,需要使用os.exit()才能退出解释器,不支持常见的退出操作和清屏指令,甚至不支持上下方向键和其它常见的快捷键;
  • luac的参数选项不能随便更改顺序,例如-o xxx放在脚本文件名后面会解析错误。

基本语法

Lua的标识符通常包括大小写字母,下划线和数字,不允许以数字开头,区分大小写。 以下划线加大写字母形式的标识符为Lua的保留标识符,建议不要使用。

Lua 的保留关键词如下

1
2
and break do else elseif end false for function if in
nil not or repeat return then true until while goto

Lua支持单行和多行注释,示例如下

1
2
3
4
5
6
--两个减号是单行注释

--[[
这是多行注释
这是多行注释
--]]

虽然#不被视作注释,Lua脚本对形如#!/path/to/lua的脚本开头行也是支持的。

和Python类似,Lua的语句可以但是不要求使用分号;结尾,但是如果希望把多个语句写在同一行,则必须加上分号。

在Lua中可以使用print()函数进行输出,在解释器中如果一个表达式的结果没有被变量接收,也会立刻显示出来。

变量

Lua的变量机制大体上和Python类似,变量在使用之前不需要声明,直接赋值即可。

在Lua中访问没有经过初始化的变量甚至不会出错,而是会返回nil

Lua和Python在变量部分的最大语法区别是:默认的变量总是认为是全局的,除非在定义时专门加上local修饰。 涉及作用域的具体规则如下:

  • 在默认情况下,所有变量(包括函数)都是全局的,可以在整个程序中访问。
  • 使用函数和控制结构可以创建独立的局部作用域。
  • 使用local关键字声明变量,可以将作用域改为局部的。
  • 局部变量会遮蔽同名的全局变量,在读取和修改变量时,优先查找局部变量,找不到时会向外查找全局变量。
  • 对于找不到定义的变量,赋值意味着初始化,此时会自动创建全局变量而非局部变量。

和Python类似,Lua自带了一套包括垃圾回收的内存管理机制,我们不需要也没有办法直接管理内存。

基本数据类型

Lua 有八个基本数据类型:nilbooleannumberstringuserdatafunctionthreadtable, 我们主要关注其中最基础的前四个类型。

使用type(X)可以获取变量X的类型字符串,例如

1
2
3
4
type(x) -- nil
type(true) -- boolean
type(1.2) -- number
type('a') -- string

nil类型只有一个同名的值nil,语义与Python的None类似,但是在Lua中发挥了更大的作用:

  • 输出一个未定义的变量,会显示nil
  • 对一个变量赋值为nil,相对于删除这个变量;

例如判断一个标识符是nil(注意type()返回的结果是字符串)

1
type(x) == 'nil' -- true

boolean类型只有两个可选值:true(真) 和 false(假), 需要注意,Lua将其它类型变量转换为boolean类型时的逻辑为:

  • nil被视作false
  • 其它所有都视作true,包括数字0和空字符串''

Lua只提供了双精度浮点数这一种数值类型,没有单独提供整数类型。使用tonumber函数可以尝试将变量转换为数值

1
a = tonumber('123')

Lua的字符串可以由一对双引号或单引号包裹,例如

1
2
string1 = "this is string1"
string2 = 'this is string2'

也可以用 2 个方括号 [[]] 来表示多行的字符串。

使用..而不是通常的加号来拼接字符串

1
a = 'hello,' .. 'world.'

使用#可以计算字符串的长度(也可以用于计算表的长度)

1
2
3
4
print(#'abc') -- 3

s = 'abc'
print(#s) -- 3

字符串对于特殊字符需要使用转义处理,例如回车\n

使用tostring函数可以尝试将变量转换为字符串

1
a = tostring(123)

对一个可以转换为数字的字符串进行算术操作时,Lua会尝试将这个数字字符串转成一个数字(很离谱的语法糖)

1
print('1'+3) -- 4

Lua内置了一个string库,无需导入即可直接使用,里面提供了很多常用的字符串操作,例如

1
2
3
4
5
6
7
8
9
10
11
-- 截取字符串
print(string.sub("Hello Lua", 4, 7)) -- 'lo L'

-- 大小写转换
a = string.lower('Abc') -- 'abc'
b = string.upper('Abc') -- 'ABC'

-- 字符串格式化
d,m,y = 20,7,2024
print(string.format("%s %02d/%02d/%d", "today is:", d, m, y))
-- today is: 20/07/2024

基本运算

Lua的赋值语法非常自由,支持多对多的赋值,例如交换两个值

1
a, b = b, a

并且两侧的个数可以不相等,对应的处理逻辑为:

  • 如果左侧变量的个数少于右侧值的个数,将靠后的多余的值丢弃;
  • 如果左侧变量的个数多于右侧值的个数,将多的变量赋值nil

Lua支持常见的算术运算,包括取余%和乘方^,除法/和整除//,但是不支持+++=等简化运算。

Lua支持常见的比较运算,值得注意的是不等号是~=而不是!=

Lua 以关键词形式提供逻辑运算,包括andornot,它们支持短路运算。

流程控制

直接给几个例子即可

if条件语句

1
2
3
4
5
6
7
8
9
local n = 10

if n >= 0 and n < 5 then
print('level 1')
elseif n >= 5 and n < 10 then
print('level 2')
elseif n >= 10 then
print('level 3')
end

for循环语句

1
2
3
4
5
6
7
local result = 0

for i=1,100 do
result = result + i
end

print(result)

while循环语句

1
2
3
4
5
6
7
8
9
local result = 0
local num = 1

while num <= 100 do
result = result + num
num = num + 1
end

print(result)

在上述结构中支持breakgoto控制语句。

表 Table

Lua的table是一种强大且多功能的数据结构,类似于其他编程语言中的数组、字典或结构体。

作为事实上唯一的内置数据结构,Lua 的table可以被用来表示数组、哈希表、集合和记录(结构体)等。 例如Lua的所有全局变量都存储在名为_Gtable中。

创建

首先,Lua的表可以当作列表使用,可以直接使用字面量来创建表,{}代表空表

1
2
3
local tb1 = {}

local tb2 = {"apple", "pear", "orange", "grape"}

此时默认的索引从1开始。

其次,Lua的表在本质上更像是字典,可以用键值对的方式来创建表

1
2
3
4
5
local tb = {
['key1'] = "value1",
['key2'] = "value2",
['key3'] = "value3"
}

这种创建方式中索引通常是字符串,也支持使用数字作为索引

1
2
3
4
local t = {
[1] = 'a',
[2] = 'b'
}

这和前面的数组风格的创建方式实际是等效的,并且这里可以自定义索引的开始,不要求数字是连续的。

两种索引当然可以混合使用

1
2
3
4
5
6
local t2 = {
['key1'] = "value1",
['key2'] = "value2",
[1] = "array1",
[2] = "array2"
}

table支持嵌套定义,例如

1
2
3
4
5
6
7
8
9
10
11
12
local t = {
apple = {
price = 7.52,
weight = 2.1,
},
banana = {
price = 8.31,
weight = 1.4,
year = '2018'
},
year = '2019'
}

使用

访问table时主要通过键来读写对应的值

1
2
tb['key1'] = 'value0'
print(tb[key1])

对于以数组形式创建的表,自动使用从1开始的连续整数作为索引

1
2
print(tb2[1])
tb2[1] = 'banana'

读取使用不存在的键会返回nil,对不存在的键进行赋值会直接创建对应的项,使用nil进行赋值则代表删除对应的项。

如果键是字符串,在不引起歧义的情况下,Lua还提供如下的语法糖,使得读写类似于C语言中结构体的风格

1
2
3
4
5
6
local tb = {
['key1'] = '1',
['key2'] = '2'}

print(tb['key1'])
print(tb.key1) -- 简化版

在定义时,如果键是字符串并且不会引起歧义,也有类似的语法糖

1
2
3
4
local tb = {
key1 = '1',
key2 = '2'
}

遍历

对于使用从1开始连续整数索引的数组型table,可以使用ipairs函数进行遍历

1
2
3
4
5
6
7
8
9
10
local array = { "a", "b", "c" }
for i, value in ipairs(array) do
print(i, value)
end

--[[
1 a
2 b
3 c
]]

如果表的索引不满足要求,不适合使用这个函数进行遍历,因为ipairs的原理就是从1开始不断尝试访问元素,遇到nil就会停止。

对于更一般的表,可以使用pairs函数进行遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local t = {
key1 = "value1",
key2 = "value2",
[1] = "value3",
[2] = "value4"
}
for key, value in pairs(t) do
print(key, value)
end

--[[
1 value3
2 value4
key1 value1
key2 value2
]]

函数

基础

Lua的函数语法和其它语言没什么区别,使用function关键词,需要使用return提供返回值,例如

1
2
3
local function add(a, b)
return a + b
end

函数默认具有全局作用域,建议加上local修改为局部作用域。

Lua的函数传参机制和Python是类似的:

  • 对基本数据类型(如数值、字符串、布尔值)相当于值传递;
  • 对表(table)和函数等复杂数据类型相当于引用传递。

Lua甚至允许函数的实参和形参个数不匹配:

  • 如果实参多于形参,靠后的实参被舍弃;
  • 如果实参少于形参,不足的形参被赋值为nil

函数可以直接返回多值,不需要额外处理。

1
2
3
4
5
6
local function swap(a, b)
return b, a
end

x, y = swap(1, 2)
print(x, y) -- 2 1

和Python一样,Lua把函数视为普通类型的一种,可以直接把函数作为变量传递,不需要使用函数指针或句柄的特殊处理。

1
2
3
4
5
6
local function apply(func, a, b)
return func(a, b)
end

local result = apply(add, 2, 3)
print(result) -- 5

Lua对于可变参数使用特殊的...表示,例如

1
2
3
4
5
6
7
8
9
local function sum(...)
local s = 0
for _, v in ipairs{...} do
s = s + v
end
return s
end

print(sum(1, 2, 3, 4)) -- 10

在Lua中定义一个函数实际上等价于把一个匿名函数赋值给一个变量

1
2
3
local add = function(a,b)
return a+b
end

例如我们可以将函数赋值给表中的一项

1
2
3
4
5
local t = {}

t.add = function(a,b)
return a+b
end

它完全等价于下面的语法

1
2
3
4
5
local t = {}

function t.add(a,b)
return a+b
end

这说明在Lua在没有函数标识符的概念,只有统一的变量标识符。

Lua还提供了一个比较离谱的函数调用语法糖:如果只有一个实参,且这个实参是一个字符串字面量或者是table字面量,则可省略括号,例如

1
2
func 'abc' -- func('abc')
func {'a','b'} -- func({'a','b'})

不过相比于MATLAB在无参数调用时可以直接省略括号的离谱语法糖,Lua的这个语法糖还是可以接受的。

进阶

Lua也支持嵌套函数、闭包和高阶函数,基本与Python相同,除了默认作用域的处理:Python的变量默认是局部的,而Lua的变量默认是全局的。

函数嵌套例如

1
2
3
4
5
6
7
8
9
local function outer(a)
local function inner(b)
return a + b
end
return inner
end

local add5 = outer(5)
print(add5(3)) -- 8

闭包例如

1
2
3
4
5
6
7
8
9
10
11
local function createCounter()
local count = 0
return function()
count = count + 1
return count
end
end

local counter = createCounter()
print(counter()) -- 1
print(counter()) -- 2

高阶函数例如

1
2
3
4
5
6
7
8
9
10
11
12
13
local function derivative(f, delta)
delta = delta or 1e-4
return function(x)
return (f(x + delta) - f(x)) / delta
end
end

local function square(x)
return x * x
end

local dsquare = derivative(square)
print(dsquare(5))

I/O

基本I/O

print函数用于向标准输出打印信息,支持多个参数,并在输出的各项之间自动添加空格。

1
2
print("Hello, world!")  -- 输出: Hello, world!
print("The answer is", 42) -- 输出: The answer is 42

io.write用于输出到标准输出,不自动添加空格或换行符。

1
2
io.write("Hello, world!")  -- 输出: Hello, world!
io.write("The answer is", 42) -- 输出: The answer is42

io.read:用于从标准输入读取数据。可以指定读取模式,如整行读取、按字符读取等。

1
2
3
4
5
local line = io.read()  -- 读取一行输入
print("You entered:", line)

local char = io.read(1) -- 读取一个字符
print("You entered:", char)

文件I/O

Lua 提供了对文件进行读写的操作,需要通过 io 库的函数实现,文件操作对各个语言都是类似的,直接提供几个例子即可。

io.open函数以各种模式打开文件,并返回文件句柄

1
local file = io.open("example.txt", "r")

读取文件内容

1
2
3
4
local file = io.open("example.txt", "r")
local content = file:read("*all") -- 读取整个文件
print(content)
file:close() -- 关闭文件

写入文件内容

1
2
3
local file = io.open("example.txt", "w")
file:write("Hello, Lua!") -- 写入内容
file:close() -- 关闭文件

错误处理

使用error函数可以直接生成一个错误

1
error("This is an error message!")

使用assert函数可以创建断言: 如果条件为假,则生成一个错误。

1
2
assert(1 == 1, "Condition failed!")
assert(1 == 2, "Condition failed!") -- 报错: Condition failed!

使用pcall函数来调用一个函数,可以捕获该函数中出现的任何错误

1
2
3
4
5
6
7
8
9
local status, result = pcall(function()
error("An error occurred!")
end)

if status then
print("Function succeeded:", result)
else
print("Function failed:", result)
end

模块与包

Lua 提供了模块化编程的支持,可以将代码组织成独立的模块和包,使用 module 表和 require 函数可以创建和加载模块。

例如我们创建一个名为 mymodule.lua 的文件,内容如下

1
2
3
4
5
6
7
local mymodule = {}

function mymodule.sayHello()
print("Hello, from mymodule!")
end

return mymodule

这里将函数放在了表中,并且将这个表返回,这样就得到了一个名为mymodule的模块。

在其他文件中,可以使用 require 加载并使用模块

1
2
local mymodule = require("mymodule")
mymodule.sayHello() -- Hello, from mymodule!

Lua 使用 package.path 来确定搜索模块的路径,我们需要修改 package.path 来添加自定义路径,确保Lua可以找到对应模块

1
2
package.path = package.path .. ";./my_modules/?.lua"
local mymodule = require("mymodule")