显示文件名与行号、多等级、多输出打印、支持循环表的 Lua 打印器模块实现

模块简介

本文提供一款在 Lua 中实现的显示文件名与行号、多等级、多输出打印、支持循环表的打印器模块。此模块主要参照《饥荒:联机版》 Lua 源码改写完成,在此表示感谢。

环境

  • Lua 5.1.5

模块特性

  • 支持多种输出级别,比如 Error Warning, Info
  • 支持所有 Lua 类型,包括循环表
  • 支持输出到多个目标,例如同时输出到终端及某个集合中(例如饥荒的游戏内控制台)
  • 支持显示打印函数所在文件名与行号(支持开启和关闭)

使用方法

  • 使用前require("debugPrint"),然后正常使用 print.
  • PRINT_SOURCE 设置为 true 即可打印行号与文件名。
  • 使用 VERBOSITY_LEVEL 设置打印级别,高于当前打印级别的 Print 均不会输出。级别从低到高依次为 ERROR, WARNING, INFO, DEBUG

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require("debugPrint")

print("hello")

print("hello", "world")
PRINT_SOURCE = false
print("a simple table", {10, 20})

VERBOSITY_LEVEL = VERBOSITY.INFO
Print(VERBOSITY.INFO, 'This is a info level message: ', "hello")
Print(VERBOSITY.DEBUG, 'This is a debug level message: ', "hello")
VERBOSITY_LEVEL = VERBOSITY.DEBUG
Print(VERBOSITY.INFO, 'This is a info level message: ', "hello")
Print(VERBOSITY.DEBUG, 'This is a debug level message: ', "hello")

testTable = {}
testTable[#testTable + 1] = {1, 2, 3}
testTable[#testTable + 1] = testTable
testTable[#testTable + 1] = function () oldprint("hello") end
testTable[#testTable + 1] = true
print("a complicated table", testTable)

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@D:\tmp\lua\test.lua(3,1) hello 
@D:\tmp\lua\test.lua(5,1) hello world
a simple table
10
20

This is a info level message: hello
This is a info level message: hello
This is a debug level message: hello
a complicated table
1
2
3

recursive table: 0x0015c1d0
function
true

模块代码

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
-- debugPrint.lua
-- a print implementation suitable for debug purpose
--[[
# Detailed info about this script

## Features
* Prioritized msg level
* Print all lua types including table (support recursive table)
* Print to multiple loggers (console, log table, etc. You can add your own)
* Toggle position info (line number, file name, etc.) of print statement with global switch

Compared with individual implmentation of print, info, warn or error, the advantage of this implementation is code-reuse.

## Usage
* `require("debugPrint")` and you can use `print`.
* set `PRINT_SOURCE` to `true` to print position info

## Compatibility
Tested with Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio

## Major Reference
Lua source of Don't Starve Together by Klei Entertainment (https://www.klei.com/)
Lua source of Garry's Mod by Facepunch Studios LTD (http://www.facepunchstudios.com/)

## TODO
[] print large table more effectively
[] have some hierarchy in print to show table hierarchy
[] get more detailed message for function

## Legal Notice
This file is licensed under The MIT License (MIT)
Copyright (c) 2018 HustLion <hustlion-dev@outlook.com>
--]]

PRINT_SOURCE = true
CWD = "" -- you can set a global value CWD to current working directory and the log will include it

local print_loggers = {}
local dir = CWD
dir = string.gsub(dir, "\\", "/") .. "/"
local oldprint = print

-- string ops from https://github.com/Facepunch/garrysmod/blob/master/garrysmod/lua/includes/extensions/string.lua
function string.Explode(separator, str, withpattern)
if ( separator == "" ) then return totable( str ) end
if ( withpattern == nil ) then withpattern = false end

local ret = {}
local current_pos = 1

for i = 1, string.len( str ) do
local start_pos, end_pos = string.find( str, separator, current_pos, not withpattern )
if ( not start_pos ) then break end
ret[ i ] = string.sub( str, current_pos, start_pos - 1 )
current_pos = end_pos + 1
end

ret[ #ret + 1 ] = string.sub( str, current_pos )

return ret
end

function string.split( str, delimiter )
return string.Explode( delimiter, str )
end

function AddPrintLogger( fn )
table.insert(print_loggers, fn)
end

-- https://stackoverflow.com/a/7574047/4394850
local function toarray(...)
return {...}
end

local function _packTable (t, tableRecorder)
tableRecorder[t] = true
local str = ""
-- for small table
for i,v in ipairs(t) do
if v then
if type(v) == "table" then
if tableRecorder[v] ~= true then
tableRecorder[v] = true
str = str.._packTable(v, tableRecorder).."\n"
else
str = str.."recursive "..tostring(v).."\n"
end

elseif type(v) == "function" then
-- TODO: get function name or code?
str = str.."function".."\n"
else
str = str..tostring(v).."\n"
end

end
end
-- TODO for large one use stack
return str
end

-- e.g. oldprint(packTable(debugstr))
local function packTable(t)
local rec = {}
return "\n".._packTable(t, rec)
end

local function pack(v)
if type(v) == "table" then
return packTable(v).." "
elseif type(v) == "function" then
return "function".." "
else
return tostring(v).." "
end

end


local function packArg(...)
local str = ""
local n = select('#', ...)
-- oldprint("n is", n, "for", ...)
if n > 1 then
local args = toarray(...)
for i=1, n do
-- str = str..tostring(arg[i]).."\t"
str = str..pack(args[i])
end
return str
else
return pack(...)
end
end

--this wraps print in code that shows what line number it is coming from, and pushes it out to all of the print loggers
print = function(...)

local str = ""
if PRINT_SOURCE then
local info = debug.getinfo(2, "Sl") -- print function is call stack 1, and the caller is 2
local source = info and info.source
if source then
str = string.format("%s(%d,1) %s", source, info.currentline, packArg(...))
else
str = packArg(...)
end
else
str = packArg(...)
end
-- oldprint("str for loggers:", str)


for i,v in ipairs(print_loggers) do
v(str)
end

end

-- TODO: This is for times when you want to print without showing your line number (such as in the interactive console)
local nolineprint = function(...)
for i,v in ipairs(print_loggers) do
v(...)
end
end

---- This keeps a record of the last n print lines, so that we can feed it into the debug console when it is visible
local debugstr = {}
local MAX_CONSOLE_LINES = 20

local consolelog = function(...)

local str = packArg(...)
str = string.gsub(str, dir, "")

for idx,line in ipairs(string.split(str, "\r\n")) do
table.insert(debugstr, line)
end

while #debugstr > MAX_CONSOLE_LINES do
table.remove(debugstr,1)
end
end

local textlog = function (...)
oldprint(...)
end


function GetConsoleOutputList()
return debugstr
end

-- add our print loggers
AddPrintLogger(consolelog)
AddPrintLogger(textlog)

-- Prioritized logger
VERBOSITY =
{
ERROR = 0,
WARNING = 1,
INFO = 2,
DEBUG = 3,
}

--VERBOSITY_LEVEL = VERBOSITY.WARNING
VERBOSITY_LEVEL = VERBOSITY.DEBUG
function Print( msg_verbosity, ... )
if msg_verbosity <= VERBOSITY_LEVEL then
print( ... )
end
end


---- Testing

-- Print(VERBOSITY.DEBUG, 'This is a debug level message: ', "hello")

-- print("hello")
-- PRINT_SOURCE = false
-- print("hello", "world")
-- print("table", {10, 20})

-- debugstr[#debugstr + 1] = {1, 2, 3}
-- debugstr[#debugstr + 1] = debugstr
-- debugstr[#debugstr + 1] = function () oldprint("hello") end
-- debugstr[#debugstr + 1] = true
-- oldprint("Printing the recursive debugstr table")
-- print(debugstr)

实现关键

  • 循环表打印时对已经遇到/打印的表进行记录,防止循环进入打印。思路来自《Lua程序设计》
  • 通过 {...}... 转化为数组
  • 将生成字符串和输出字符串分成两个阶段,在输出阶段通过代表不同输出端的 logger 来支持多端输出。
  • 对于需要级别的输出,提供带级别的 Print 函数来完成。
  • 使用全局变量 VERBOSITY_LEVEL 控制输出级别,全局变量 PRINT_SOURCE 控制是否输出行号

示例工程

备注

TODO

  • [] print large table more effectively
  • [] have some hierarchy in print to show table hierarchy
  • [] get more detailed message for function

This file is licensed under The MIT License (MIT)
Copyright (c) 2018 HustLion hustlion-dev@outlook.com

参考资料