在 Lua 中使用 ANSI 转义码
Lua 没有 \e 转义字面,但 \27(十进制)在 Lua 5.1 起的所有解释器中都可用,\x1b(十六进制)自 Lua 5.2 起可用。io.write 与 print 在 PUC-Rio Lua(5.1 / 5.2 / 5.3 / 5.4)、LuaJIT 以及随 Neovim、OpenResty、Redis、Wireshark 嵌入的解释器中均按字节透传。注意:长括号字符串 [[...]] 不展开转义序列 —— 只有常规 "..." 字符串会展开,因此输出 ANSI 字节时必须使用双引号字符串。
人体工学方面:ansicolors.lua(kikito 的经典 gem)将 colors('%{red bold}error:%{reset} permission denied') 之类的模式字符串解析为 SGR 序列 —— 单文件依赖,可经 LuaRocks 安装或直接 vendor 拷入。term.lua(hoelzro 移植自 kikito 的终端控制库)覆盖非 SGR 一侧:term.clear()、term.cleareol()、term.cursor.goto(x, y)、term.cursor.hide()、备用屏 + 光标保存恢复。完整全屏 TUI 链接 lcurses(Lua 的 ncurses 绑定)。能力门控 —— isatty()、TIOCGWINSZ、termios 原始模式 —— 用 LuaPosix。
推荐库
- ansicolors.lua
基于模式的颜色辅助 ——
colors('%{red bold}error:%{reset} permission denied')。模式标记%{<style> <color> on_<bgcolor>}展开为 SGR 转义序列。单文件依赖,可通过 LuaRocks 安装(luarocks install ansicolors)或直接 vendor 拷贝。被busted、inspect及众多 LÖVE 游戏用于引擎内控制台输出。 - term.lua (hoelzro)
终端控制辅助 ——
term.clear()、term.cleareol()、term.cursor.goto(x, y)、term.cursor.hide()/.show()、.save()/.restore()、备用屏辅助。与 ansicolors.lua 搭配:本库处理光标 + 屏幕,ansicolors 处理 SGR。约 150 行,无原生依赖。 - lcurses
Lua 的 ncurses 绑定 —— 窗口、面板、菜单、表单、配色对、鼠标、原始输入。用于全屏 TUI(文件管理器、仪表板、文本模式游戏)。通过 LuaRocks 安装(
luarocks install lcurses),构建期需 ncurses 头文件。 - LuaPosix
POSIX 绑定 ——
posix.unistd.isatty(1)用于 stdout 能力检测、posix.termio.tcgetattr/tcsetattr切换原始模式、posix.sys.ioctl配合TIOCGWINSZ查询终端尺寸。在脚本可能被管道、CI 或哑终端执行时进行能力门控所必备。
常用写法
-- Lua has no \e literal. Use \27 (decimal, works in every Lua) or
-- \x1b (hex, requires Lua 5.2+). Note: long-bracket strings [[...]] do
-- NOT expand escapes — always use regular "..." strings for ANSI.
io.write("\27[1;31merror:\27[0m permission denied\n")
-- Lua 5.2+:
io.write("\x1b[1;32mok:\x1b[0m all 142 tests passed\n")local colors = require 'ansicolors'
print(colors('%{red bold}error:%{reset} permission denied'))
print(colors('%{green}ok:%{reset} all 142 tests passed'))
print(colors('%{white on_blue bold} highlighted %{reset} back to normal'))
print(colors('%{yellow}deprecated:%{reset} use the new API'))-- Capability gate: NO_COLOR > FORCE_COLOR > !isatty(stdout). Wrap the
-- LuaPosix require in pcall so the script still runs on hosts without it
-- (Windows without WSL, locked-down embedded interpreters).
local ok, unistd = pcall(require, 'posix.unistd')
local is_tty = ok and unistd.isatty(1) == 1 -- 1 = STDOUT_FILENO
local function should_color()
if os.getenv('NO_COLOR') ~= nil then return false end
local force = os.getenv('FORCE_COLOR')
if force and force ~= '0' then return true end
return is_tty
end
local USE_COLOR = should_color()
local function paint(sgr, text)
if USE_COLOR then
return ('\27[%sm%s\27[0m'):format(sgr, text)
end
return text
end
print(paint('1;31', 'error:'), 'permission denied')
print(paint('1;32', 'ok:'), 'all 142 tests passed')-- Hide cursor for the duration of the progress loop; restore it on
-- normal exit AND on Ctrl-C. Lua's poor man's defer is an __gc finalizer
-- on a sentinel table — runs when the script exits.
io.write("\27[?25l") -- hide cursor
local _restore = setmetatable({}, {__gc = function()
io.write("\27[?25h") -- restore cursor
end})
for i = 1, 100 do
-- \r returns to col 1; \27[K erases to end of line
io.write(("\r\27[K%d%% complete"):format(i))
io.flush()
os.execute('sleep 0.02') -- portable; LuaSocket has socket.sleep()
end
io.write("\n")