在 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")