跳到主要内容
ansicode

野外的转义码:解码方案

遇到一行带转义码的日志、一段录制的 TUI 会话、或一份散落的 .ans 文件?挑下面任一方案直接管道解码 —— 或把书签小工具拖到书签栏,选中任何文字一键送入本站解码器。

最近更新

转义码会从哪里冒出来

CI 日志最常见 —— GitHub Actions 与 GitLab 在网页 UI 渲染时会剥掉 SGR,但归档的原始字节里完好保留,下载原始日志再 `less -R` 一下色彩就回来了。`script(1)` 录的 typescript 与 asciinema cast 都按字面记录每条转义(asciinema 还保留时间戳,方便回放)。容器运行时和 systemd 把应用 stdout 原样透传,哪怕 `docker logs` / `kubectl logs` / `journalctl` 都不是 TTY —— 这就是终端里 `^[[31m` 字面噪音的源头。日志收集器各占一边:Loki 与 Vector 默认保留 SGR;fluentbit / fluentd 通常用 filter 插件剥掉。dotfiles + prompt 框架(oh-my-zsh、starship、p10k)是另一大源头 —— 每个主题都是一片 `\x1b[` 序列的海。从下方挑一条贴合你场景的菜谱。

书签小工具 —— 把选中文本送入解码器

把下面这条链接拖到浏览器书签栏。在任何页面选中带转义码的文本后点书签,会自动新开一个 ansicode.eversources.app/decode 页面并把文本预填好,立刻完成分词与渲染。

Decode ANSI

书签源码
javascript:(function(){var t=window.getSelection().toString();if(!t){t=window.prompt("粘贴含转义码的文本:")||'';}if(t){window.open("https://ansicode.eversources.app/zh/decode"+'?text='+encodeURIComponent(t),'_blank','noopener');}})();void(0);

如果浏览器拖放时把链接吞掉了,复制下方源码,并手动建一个新书签把它粘到 URL 栏。

CLI 配方

不离开终端的解码方式 —— 按场景挑一条。

curl + python —— 内联解码远程日志
# Stream a remote log through an inline tokenizer
# (replace the URL with whatever stream you have):
curl -fsSL https://example.com/build.log | python3 -c '
import sys, re
ANSI = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))")
data = sys.stdin.read()
i = 0
for m in ANSI.finditer(data):
    sys.stdout.write(data[i:m.start()])
    sys.stdout.write(f"«{m.group(0)!r}»")
    i = m.end()
sys.stdout.write(data[i:])
'

把任意命令输出管道给一段简短的内联 Python:每个转义被标注,其它文本原样通过。适合 CI 日志、journalctl 或任何不能 `less -R` 的流。

Node.js 单行 —— JavaScript 版的内联分词
# JavaScript / Node parity with the Python recipe above.
# Same regex (CSI + OSC + bare ESC), same « … » wrapping output.
# Pure Node 18+ — no npm install, no transpile.
curl -fsSL https://example.com/build.log | node -e '
const ANSI = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
process.stdin.setEncoding("utf8");
let buf = "";
process.stdin.on("data", (c) => { buf += c; });
process.stdin.on("end", () => {
  process.stdout.write(buf.replace(ANSI, (m) => "«" + JSON.stringify(m) + "»"));
});
'

# For a live tail:
# tail -F app.log | node -e '...'

Python 配方的 JavaScript 对照版 —— 同样的正则、同样的 `«…»` 括号包裹输出。适合构建环境只有 Node 没有 Python 的场景(仅 npm 的 CI 镜像、Electron 项目、JS monorepo 开发机)。纯 Node 18+ 就能跑,不用 `npm install`、不用转译。TypeScript 项目用 `tsx -e '…'` 直接接受同样的脚本主体。

Deno 2+ / TypeScript —— 带能力门控的类型化内联分词
# Deno 2+ on TypeScript — typed, no npm install, no transpile step.
# Stdin isn't capability-gated, so the inline form runs with zero flags.

curl -fsSL https://example.com/build.log | deno eval '
const ANSI = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
const dec = new TextDecoder();
let buf = "";
for await (const chunk of Deno.stdin.readable) buf += dec.decode(chunk);
console.log(buf.replace(ANSI, (m) => "«" + JSON.stringify(m) + "»"));
'

# Saved + typed version (decode-ansi.ts) — same logic, full TypeScript:
#   deno run decode-ansi.ts < build.log
const ANSI: RegExp = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
const dec = new TextDecoder();
let buf = "";
for await (const chunk of Deno.stdin.readable) buf += dec.decode(chunk);
console.log(buf.replace(ANSI, (m: string) => `«${JSON.stringify(m)}»`));

# Network-fetch variant — capability-gated to one host, no shell-pipe needed:
deno eval --allow-net=example.com '
const ANSI = /\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
const txt = await (await fetch("https://example.com/build.log")).text();
console.log(txt.replace(ANSI, (m) => "«" + JSON.stringify(m) + "»"));
'

正则与 Node 配方完全相同,只是用 Deno 的惯用写法表达:stdin 是 Web streams 风格的 `Deno.stdin.readable`(`ReadableStream<Uint8Array>`),用标准 `TextDecoder` 解码。三种形态:(a) **内联 `deno eval`** —— 零安装、零参数,可直接接到管道日志流上。(b) **类型化的保存脚本**(`decode-ansi.ts`)—— 同样的逻辑加上 TypeScript 注解,`deno run` 会替你做类型检查;适合 repo dotfiles + CI 脚本。(c) **网络抓取形态**,显式 `--allow-net=example.com` 能力门控 —— Deno 的白名单模型让只需要抓一个主机的脚本能被沙盒到仅此一个主机,区别于 Node 的全有或全无网络访问。何时选 Deno 而非 Node:目标机器已装 Deno(一次性工具免去 `npm install`)、想要类型化代码却不想配 `tsconfig.json` + `tsx` 工具链、或在乎沙盒保证(不可信日志源、供应链敏感环境)。

Rust + regex::bytes —— 基于字节的内联分词器(rust-script)
# Rust + regex::bytes — byte-safe inline tokenizer.
# regex::bytes skips UTF-8 boundary checks (inner bytes inside CSI / OSC
# are guaranteed ASCII by spec), so it's the right choice for log streams
# whose encoding around the escapes isn't yours to control.

# Single-file form via rust-script — drop a #!-prefixed .rs with an
# inline Cargo manifest, chmod +x, treat it like a Python script.
cargo install rust-script   # one-time

cat > decode-ansi.rs <<'EOF'
#!/usr/bin/env rust-script
//! ```cargo
//! [dependencies]
//! regex = "1"
//! ```
use regex::bytes::Regex;
use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    let mut buf = Vec::new();
    io::stdin().read_to_end(&mut buf)?;
    let ansi = Regex::new(
        r"\x1b(?:[@-Z\\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))"
    ).unwrap();
    let mut last = 0;
    let mut out = io::stdout().lock();
    for m in ansi.find_iter(&buf) {
        out.write_all(&buf[last..m.start()])?;
        write!(out, "«{}»", buf[m.range()].escape_ascii())?;
        last = m.end();
    }
    out.write_all(&buf[last..])
}
EOF
chmod +x decode-ansi.rs
curl -fsSL https://example.com/build.log | ./decode-ansi.rs

# Cargo project form — what you'd commit alongside a TUI in ratatui /
# crossterm / termion so the same regex serves byte-stream + render:
#   cargo new --bin decode-ansi && cd decode-ansi
#   # add `regex = "1"` under [dependencies] in Cargo.toml
#   # paste fn main() above into src/main.rs
#   cargo run --release --quiet < build.log

# Companion crate for TUI alignment — pair with `unicode-width` (UCS9
# port) rather than libc `wcwidth(3)`; results disagree across glibc /
# musl / macOS. See /pitfalls/wcwidth-disagreement.

Rust 与 Python / Node / Deno 配方的对照版 —— 相同的 CSI / OSC / 裸 ESC 正则,相同的 `«…»` 括号包裹输出。用 `regex::bytes::Regex` 而非默认的 `&str` 版,因为转义封套内部字节按规范保证是 ASCII —— 没有 UTF-8 边界要兼顾,字节版正则对编码不受控的日志流更快。两种交付形态:(a) **`rust-script` 单文件** —— 写一个 `#!` 开头的 `.rs`、内嵌 Cargo 清单、`chmod +x`,当 Python 脚本用(一次性 `cargo install rust-script`)。(b) **普通 Cargo bin** —— 与 `ratatui` / `crossterm` / `termion` TUI 一起提交时该用的形态,让同一个正则同时服务字节流与渲染端。切片方法 `escape_ascii()`(1.60 起稳定)把匹配字节渲染成可打印的 `\x1b[31m` 形式,零中间 `String` 分配。TUI 布局需要列宽时配 `unicode-width`(UCS9 的 Rust 实现)—— libc 的 `wcwidth(3)` 在 glibc / musl / macOS 之间结果不一致,固定一个 Unicode 版本号是更稳的做法(参见 `/pitfalls/wcwidth-disagreement`)。

Go + regexp(RE2)—— 不会回溯的内联分词器
# Go + regexp (RE2) — backtrack-free inline tokenizer.
# Go's stdlib regexp is RE2 — guaranteed linear-time on hostile input, so
# you can run it across untrusted log streams without catastrophic-regex
# risk. Same envelope as the Python / Node / Deno / Rust recipes: CSI +
# OSC + bare-ESC, each match wrapped in « … ».

# Single-file form via 'go run' — no module init, no install, no binary
# on disk. Great for one-shot pipeline use.
cat > decode-ansi.go <<'EOF'
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"regexp"
)

var ansi = regexp.MustCompile(
	"\x1b(?:[@-Z\\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))",
)

func main() {
	buf, err := io.ReadAll(bufio.NewReader(os.Stdin))
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	out := ansi.ReplaceAllFunc(buf, func(m []byte) []byte {
		return []byte(fmt.Sprintf("«%q»", m))
	})
	os.Stdout.Write(out)
}
EOF
curl -fsSL https://example.com/build.log | go run decode-ansi.go

# Module form — what you'd commit alongside a TUI in cobra / bubbletea /
# lipgloss / fang so the same regex serves byte-stream + render:
#   mkdir decode-ansi && cd decode-ansi
#   go mod init example.com/decode-ansi
#   # paste the file above into main.go
#   go build -o decode-ansi && ./decode-ansi < build.log

# Capability detection — pair with golang.org/x/term for isatty + colour
# gating (the same pattern the charmbracelet stack uses):
#   if term.IsTerminal(int(os.Stdout.Fd())) { /* emit colour */ }
# See /use/go for fatih/color + lipgloss usage.

Go 标准库 `regexp` 是 RE2 —— 对恶意输入保证线性时间,所以在不可信日志流上跑分词器没有 Python `re` 那种灾难性正则风险。与 Python / Node / Deno / Rust 配方共用同一条 CSI / OSC / 裸 ESC 封套正则、同样的 `«…»` 括号包裹输出。两种交付形态:(a) **`go run` 单文件** —— 不用 `go mod init`、不安装、磁盘上没有二进制产物,源文件就是脚本本身。一次性管道场景的首选。(b) **模块形态** —— `go mod init` + `go build -o decode-ansi` 产出一个独立的静态二进制,可与 `cobra` / `bubbletea` / `lipgloss` / `fang` 写的 TUI 一并提交,让同一个正则同时服务字节流与渲染端。能力检测(`isatty` + 颜色门控)配合 `golang.org/x/term` 的 `term.IsTerminal(int(os.Stdout.Fd()))` —— charmbracelet 全家桶用的也是这套模式。`fatih/color` 与 `lipgloss` 的库选型参见 `/use/go`。

PowerShell —— 在 Windows Terminal / PS 7+ 上发出与解码 ANSI
# PowerShell 7+ on Windows Terminal — emit + decode ANSI natively.
# (Windows PowerShell 5.1 still ships with Windows; escape hatch below.)

# Emit colour: PS 7+ has `e as the ESC escape inside double-quoted strings:
Write-Host "`e[1;31mERROR`e[0m something broke"

# Windows PowerShell 5.1 (no `e support) — build ESC manually:
$e = [char]27
Write-Host "$e[1;31mERROR$e[0m something broke"

# Decode an escape-laden file: wrap every CSI / OSC / bare-ESC sequence
# in « … » brackets so they're visible in the host. Pure PowerShell —
# .NET regex handles \e (ESC) and \a (BEL) natively, no extra escaping.
Get-Content build.log -Raw -Encoding UTF8 |
  ForEach-Object { [regex]::Replace($_,
    '\e(?:[@-Z\\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\a\e]*(?:\a|\e\\))',
    '«$&»') } |
  Out-Host

# Force-on / force-off colour rendering (PS 7.2+ master switch):
$PSStyle.OutputRendering = 'Ansi'       # always render escape codes
$PSStyle.OutputRendering = 'PlainText'  # always strip escape codes
$PSStyle.OutputRendering = 'Host'       # default: TTY renders, pipe strips

# Honour the cross-platform NO_COLOR convention:
if ($env:NO_COLOR) { $PSStyle.OutputRendering = 'PlainText' }

PowerShell 7+(跨平台的 `pwsh`)在主机支持虚拟终端输出时原生渲染 ANSI —— Windows Terminal、ConEmu、VS Code 内置终端、改版后的 Windows 11 console 都符合。PS 7+ 双引号字符串里用 `` `e `` 作 ESC 速记;Windows PowerShell 5.1(仍随 Windows 一同发行)里 `` `e `` 不存在 —— 得用 `[char]27` 手动构造 ESC。解码文件的菜谱用 `[regex]::Replace` 把每条转义包进 `«…»`;.NET 正则原生识别 `\e`(ESC)与 `\a`(BEL),所以模式就是 bash 那一套不必再加转义。`$PSStyle.OutputRendering` 是 PS 7.2+ 的总开关 —— `Ansi` 强制让颜色穿过任何重定向,`PlainText` 一律剥除,`Host`(默认)在 TTY 时渲染、重定向时剥除。`$env:NO_COLOR` 的尊重方式与 bash 完全一致。

awk 单行 —— 高亮每个转义序列
# Wrap every escape sequence in « … » brackets so they are visible
# but not interpreted. Works with mawk/gawk/BSD awk:
awk '{
  gsub(/\x1b(\[[0-?]*[ -\/]*[@-~]|\][^\x07\x1b]*(\x07|\x1b\\)|P[^\x1b]*\x1b\\|[@-Z\\\\-_])/, "«&»");
  print
}' build.log

# Grep variant — show only lines that contain an SGR (CSI ... m) escape:
grep -E $'\x1b\[[0-9;]*m' build.log

把每条 CSI / OSC / DCS / 单纯 ESC 用 `«…»` 包起来,让转义可见但保留原文 —— 只读、不尝试渲染。适合在录制会话中 grep 任何 `\x1b[3`(斜体)字节。

less -R —— 就地渲染彩色日志
# Render colour log instead of seeing literal ^[[31m noise:
less -R build.log

# Persist across sessions — paginate everything with -R:
echo 'export LESS=R' >> ~/.bashrc        # or ~/.zshrc

# Note: -R (uppercase) keeps SGR/colour escapes active while preserving
# UTF-8 width math. Avoid -r (lowercase) — it renders every control
# character literally and breaks column alignment on East-Asian text.

`less` 默认会剥离控制字符,导致彩色变成 `^[[31m` 的字面噪音。`less -R`(或全局 `export LESS=R`)保留每条 CSI SGR 转义,让带色输出按作者预期显示。`-r`(小写)渲染所有控制字符但会破坏 UTF-8 宽度计算;ANSI 日志请用 `-R`。

docker / kubectl / journalctl —— 生产日志的渲染与剥除
# Container runtimes + systemd stash whatever the app wrote to stdout
# — including SGR escapes — but the default consumer (kubectl, docker,
# journalctl) is rarely a real terminal, so colour bytes pass through
# unrendered. Two recipes, depending on what you want to do with them.

# RENDER colour the way the app intended:
docker logs -f mycontainer 2>&1 | less -R
kubectl logs -f deploy/api -n prod  | less -R
journalctl -u nginx --no-pager      | less -R

# STRIP colour for grep / archival / log-aggregator ingestion:
docker logs mycontainer 2>&1 | ansifilter > clean.log     # canonical, apt install ansifilter
docker logs mycontainer 2>&1 | sed -E 's/\x1b\[[0-9;?]*[a-zA-Z]//g' > clean.log

# 'ansifilter' covers CSI + OSC + DCS comprehensively.
# The 'sed' fallback covers CSI only — fine for ~95% of real app output
# (everything SGR / DEC mode) but leaves OSC 8 hyperlinks and DCS Sixel
# data in place. Pick 'sed' when you can't add a package; 'ansifilter'
# when correctness matters.

容器运行时和 systemd 都把应用 stdout 原封不动地存下来 —— 包括 SGR 转义 —— 但默认消费者(kubectl / docker / journalctl)很少是真终端,所以颜色字节到了却不会被渲染。要看应用本意的彩色就管道接 `less -R`;要 grep、归档、或灌进不接受控制字符的日志聚合器,就用 `ansifilter` / `sed` 剥掉。`ansifilter` 全面覆盖 CSI + OSC + DCS;`sed` 备用方案只覆盖 CSI —— 真实应用 95% 的输出都够用,但 OSC 8 超链接和 DCS Sixel 数据会原样留下。

另请参阅