Effective Lua
NOTE: This is not a Lua tutorial or reference, just a place to record something interesting when I read Lua source code, which is version 5.4.3
lua command
-e flag
-- both of cases are correct
lua -e'print("hello world")
lua -e 'print("hello world")'
command prompt
- command prompt default values
_PROMPTis “> “_PROMPT2is “>> “
- we can set global values like below to change lua command prompts
export LUA_INIT='_G._PROMPT = "~ "; _G._PROMPT2 = "~~ "'
lua -- then prompts will be changed to new values
Lua global stuff
print function
- print will first call
__tostringmetamethod in value’s metatable if it exists.
local t = {}
local mt = {
__tostring = function()
return "willmafh's table"
end
}
setmetatable(t, mt)
print(t) -- will print `willmafh's table`
- number, string, boolean, and nil will print their values as normal.
- for other type values, it will try to get
__namefirst, if it’s not a string, then Lua default type name will be used.
local t = {}
local mt = {
__name = "willmafh's table"
}
setmetatable(t, mt)
print(t) -- `willmafh's table: 0x5f3167d3acf0`
local t = {}
local mt = {
-- here __name should be a string, otherwise it's meaningless
__name = function() return "willmafh's table" end
}
setmetatable(t, mt)
print(t) -- `table: 0x5f3167d3acf0`
Lua global table
-- _G is Lua's global table
print(_G)
-- following can recursively get the global table
print(_G["_G"])
print(_G._G)
Lua version
print(_G._VERSION)
warn function
- should use control message
"@on"to turn on warning message emitting first warn(msg1, msg2, ...)function can concat multiple string message- then should use control message
"@off"to turn off warn function - there are only two control messages,
"@on"/"@off", other messages start with ‘@’ are normal messages
warn("@on")
warn("hello world") -- Lua warning: hello world
warn("hello", " from willmafh") -- Lua warning: hello from willmafh
warn("@off")
warn("hello world") -- won't log this warning message
setmetatable function
setmetatablewill return its first argument, that is the table itself, it won’t create a new table
local t = {}
local mt = {}
local rt = setmetatable(t, mt)
print(t, rt) -- here t and rt are the same value
- if the object’s metatable has
__metatablefield, it means the metatable is protected, and we can’t use setmetatable again to change it
local mt = {
__metatable = "the metatable is protected",
__tostring = function()
return "willmafh's table"
end
}
local t = setmetatable({}, mt)
setmetatable(t, {}) -- error throw here
nilmetatable argument will clear table’s original metatable if there is any
local mt = {
__tostring = function()
return "willmafh's table from __tostring"
end
}
local t = setmetatable({}, mt)
print(getmetatable(t)) -- table: 0x603284328ef0
setmetatable(t, nil)
print(getmetatable(t)) -- nil
getmetatable function
- if the object’s metatable has
__metatable, then returns its value, it can be any type. otherwise returns the metatable itself
local mt = {
__metatable = "the metatable is protected", -- can be any type value
__tostring = function()
return "willmafh's table"
end
}
local t = setmetatable({}, mt)
print(getmetatable(t)) -- __metatable value is printed
require function
requirefunction will first searchregistry["_LOADED"]table, and if there is a required module, then it return directlypackagetable is the upvalue ofrequirefunction
local registry = debug.getregistry()
--[[
string table
table: 0x5efa18719710
table: 0x5efa18719710
--]]
local name, upvalue = debug.getupvalue(require, 1)
print(type(name), type(upvalue))
print(upvalue)
print(_G["package"])
error function
- not only can log string messages, but can log error objects, which has
__tostringmetamethod in its metatable
local errobj = setmetatable({}, {
__tostring = function()
return "error from tostring meta method"
end
})
error(errobj)
- why does
errornever returns? Actually, all things the function under error does is to prepare an error message and put it on the top of the stack, then callingLUAI_THROWmacro, which is implemented bylongjmpin c, so we will back to whereLUAI_TRYis called, which is obviously implemented bysetjmpin c, finally will callreportfunction to log the error message
arg table
- lua will collect all arguments in
argglobal table. The script name will be at index 0, and the first argument will be at index 1 and so on. The interpreter name and its options before the script will go to negative indices.Indices <= 0belong to the associative part of the table, andindices > 0belong to the array part
lua -W arg_test.lua hello world
local arg = _G.arg
print(#arg) -- 2, array part of the arg table
--[[
1 hello
2 world
--]]
for i, v in ipairs(arg) do
print(i, v)
end
--[[
1 hello
2 world
0 arg_test.lua
-2 lua
-1 -W
--]]
for k, v in pairs(arg) do
print(k, v)
end
registry table
- the
firstelement in registry table array is the main Lua thread, and thesecondelement in registry table array is the global table_G
local registry = debug.getregistry()
-- thread: 0x5886909e72a8
print(registry[1]) -- main thread
--[[
table: 0x5886909e7c50
table: 0x5886909e7c50
--]]
print(registry[2]) -- global table _G
print(_G)
- all
requireloaded modules, including lua file module and c libs module, are kept inregistry["_LOADED"]table.
require "lfs"
require "cjson.safe"
local registry = debug.getregistry()
for k, v in pairs(registry["_LOADED"]) do
print(k, v)
end
--[[
_G table: 0x6461ef84bc50
os table: 0x5786711edbb0
...
lfs table: 0x6461ef8526f0
cjson.safe table: 0x6461ef855a80
...
--]]
- all loaded c libs will be kept in
registry["_CLIBS"]table and each lib will be kept in two ways, one is key value way, the other is an array element
local lfs = require "lfs"
local cjson = require "cjson.safe"
local registry = debug.getregistry()
local clibs = registry["_CLIBS"]
--[[
1 userdata: 0x567153cf7fe0
2 userdata: 0x567153cfa700
/usr/local/lib/lua/5.4/lfs.so userdata: 0x567153cf7fe0
/usr/local/lib/lua/5.4/cjson.so userdata: 0x567153cfa700
--]]
for k, v in pairs(clibs) do
print(k, v)
end
--[[
1 userdata: 0x567153cf7fe0
2 userdata: 0x567153cfa700
--]]
for i, v in ipairs(clibs) do
print(i, v)
end
registry["_PRELOAD"]is normally an empty table, but it can be used to store loaders for lua modules, and whenrequirefunction load a new module, it will first search this table, so you can use it to do something interesting
package.preload["devtools"] = function()
return { editor = "vim", os = "linux" }
end
local devtools = require "devtools"
print(devtools.editor, devtools.os)
environment variables
LUA_NOENV
- if
luais used with-Eoption, thenLUA_NOENVwill be set true in registry
#!/usr/bin/lua5.4 -E
local registry = debug.getregistry()
print(registry["LUA_NOENV"]) -- true, since -E is used
LUA_PATH_5_4/LUA_PATH, LUA_CPATH_5_4/LUA_CPATH
;;in envsLUA_PATH_5_4/LUA_PATHmeans to insert default value from macroLUA_PATH_DEFAULTtopackage.path‘s final value;;in envsLUA_CPATH_5_4/LUA_CPATHmeans to insert default value from macroLUA_CPATH_DEFAULTtopackage.cpath‘s final value- when lua is initialized, function luaopen_package will handle all these details
--[[ /usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;/usr/local/lib/lua/5.4/?.lua;/usr/local/lib/lua/5.4/?/init.lua;./?.lua;./?/init.lua
--]]
print(package.path)
-- /usr/local/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so;./?.so
print(package.cpath)
LUA_INIT_5_4/LUA_INIT
- setup init env to do something before anything else
export LUA_INIT_5_4='@init.lua' -- using `@` to load an init file
export LUA_INIT='print("hello world from init")' -- init lua string
package table
package.config
- a string describing some compile-time configurations for packages, please refer to lua manual for details
print(package.config)
package.loaded
package.loadedis a reference toregistry["_LOADED"]
local registry = debug.getregistry()
--[[
table: 0x5c638d059a60
table: 0x5c638d059a60
--]]
print(registry["_LOADED"])
print(package.loaded)
package.preload
package.preloadis a reference toregistry["_PRELOAD"]
local registry = debug.getregistry()
--[[
table: 0x64a359eb7be0
table: 0x64a359eb7be0
--]]
print(registry["_PRELOAD"])
print(package.preload)
package.searchers
- a table contains four searching functions (currently), which is used by
requiremethod to search modules, and they are searched in the following order- preload table
- lua module, searching
package.pathone by one - c module, searching
package.cpathone by one - c root module, still searching
package.cpath, but build name by stripping components after the first dot ‘.’
- using
require "cjson.safe"as an example to illustrate the differences between searching c and searching c root
-- searching c: '/usr/local/lib/lua/5.4/`cjson/safe.so`', will try to find safe.so under cjson directory
-- searching c root: '/usr/local/lib/lua/5.4/`cjson.so`', will try to find cjson.so
local cjson = require "cjson.safe"
- both of searching c and c root will finally look for function beginning with prefix
luaopen_, such as ‘luaopen_cjson’ or ‘luaopen_cjson_safe’
package.searchpath
- searching name in the given path, which is a template like
package.path- dot ‘.’ in name will be replaced by ‘/‘
- ‘?’ in path will be replaced by dot replaced name
local name = "foo.a"
local path = "./?.lua;./?.lc;/usr/local/?/init.lua"
-- so the search order will be: './foo/a.lua', './foo/a.lc', '/usr/local/foo/a/init.lua'
package.searchpath(name, path)
package.loadlib
- it
does notperform any path searching anddoes notautomatically adds extensions likerequire, so libname arg must be the complete path - if funcname is “*” , then only loads and exports all symbols in the lib. otherwise looks for funcname and return it as a lua c function
- libs loaded by this function will be added to
registry["_CLIBS"]table
local cjson_path = "/usr/local/lib/lua/5.4/cjson.so"
print(package.loadlib(cjson_path, "*")) -- true
-- error here, since "json_encode" is not exported by cjson.so, but hey this is just an example
local json_encode = package.loadlib(cjson_path, "json_encode")
-- using function following...
table module
table.concat
sepcan be one char or more chars
local t = {"hello", "world"}
local r = table.concat(t, ",")
print(r) -- "hello,world"
local t = {"hello", "world"}
local r = table.concat(t, "||")
print(r) -- "hello||world"
- can concat only
part of the table
local t = {"prefix", "hello", "world", "postfix"}
local r = table.concat(t, ", ", 2, 3)
print(r) -- "hello, world"
table.pack
- all arguments are packed into a table, and
nfield is added with the total number of arguments
local t = table.pack("hello", "world", "you", "are", "great")
print(type(t)) -- table
print(t.n) -- 4
--[[
1 hello
2 world
3 you're
4 great
--]]
for i, v in ipairs(t) do
print(i, v)
end
table.unpack
- unpack a table and return each separate element, can unpack a whole table or just a valid range of it
local t = {1, 2, 3, 4, 5}
print(table.unpack(t)) -- 1 2 3 4 5
local t = {"hello", "world", "you're", "great"}
local n1, n2 = table.unpack(t, 2, 3)
print(n1, n2) -- world you're
os module
os.time
- current time if calling it without arguments
local now = os.time()
print(now)
- the time represented by the table argument
-- other fields will be ignored
local date_tab = {
year = 2024, -- Required
month = 1, -- Required (1-12)
day = 15, -- Required (1-31)
hour = 14, -- Optional (0-23)
min = 30, -- Optional (0-59)
sec = 45, -- Optional (0-61, for leap seconds)
isdst = false -- Optional (daylight saving time)
}
local timepoint = os.time(date_tab)
print(timepoint)
os.date
- current date string if calling it without any arguments
print(os.date()) -- Sun Dec 14 21:55:00 2025
- If format is
not *t, then date returns the date as a string, formatted according to the same rules as the ISO C functionstrftime - If format starts with ‘!’, then the date is formatted in UTC and if format is the string
*t, then date returns a table with the following fields
local date_tab = os.date("!*t")
print(type(date_tab)) -- table
--[[
sec 11
yday 348
wday 1
min 3
month 12
hour 14
year 2025
isdst false
day 14
--]]
for k, v in pairs(date_tab) do
print(k, v)
end
os.execute
- to test whether there is a shell, calling it without any arguments
local shell = os.execute()
print(shell) -- true/false
- execute a command, just a wrapper of the
systemlibrary function
local ok, what, code = os.execute("ls")
print(ok, what, code) -- true exit 0
-- if the command was terminated by a signal, then the what string will be `signal`
local ok, what, code = os.execute("hello")
print(ok, what, code) -- nil exit 127
math module
math.pi
- if you need to get
pi‘s value
print(math.pi) -- 3.1415926535898
math.huge
- the float value HUGE_VAL, a value greater than any other numeric value
print(math.huge) -- inf
math.maxinteger and math.mininteger
- current platform’s maximum and minimum integer values
print(math.maxinteger) -- 9223372036854775807
print(math.mininteger) -- -9223372036854775808
math.max and math.min
- as the name suggests, returning max and min value of arguments of each function. It would be better to combine with table.unpack function
local t = {2, 9, 1, 5, 7}
print(math.max(table.unpack(t))) -- 9
print(math.min(table.unpack(t))) -- 1
math.randomseed and math.random
- calling
math.randomseedwithout arguments formath.randomto generate random number each time
math.randomseed()
print(math.random()) -- result will be different each time math.random is called
- but calling
math.randomseedwith fixed arguments formath.randomto generate the same number each time, this is good fortesting only
math.randomseed(1, 10)
print(math.random()) -- result will be the same each time math.random is called
misc stuff
\z escape sequence
It skips the following span of whitespace characters, including line breaks; it is particularly useful to break and indent a long literal string into multiple lines without adding the newlines and spaces into the string contents
local s = "This is a very long string that \z
can be broken into multiple lines in the source \z
code for readability without adding actual newlines \z
to the string's content."
print(s)
local s = "\z hello \z world"
print(s) -- 'hello world'