在 R 中使用 ANSI 转义码 —— \033、crayon、fansi::strip_sgr
R 在双引号字符串中支持 `\033`(八进制)—— 自 R 诞生以来所有版本中最具可移植性的 ESC 字面形式。`cat("\033[31mred\033[0m\n")` 在每一个解释器、以及 macOS、Linux、BSD、现代 Windows(Windows Terminal / Conhost 1709+ 原生解析 ANSI)上都可工作。现代 R(≥ 4.0)也接受 `"\u{1b}"` Unicode 与 `"\x1b"` 十六进制(严格 2 位)转义。标准辅助选 **`crayon`** —— 事实上的 R ANSI 库,被 tidyverse、testthat、pkgdown、devtools 使用:`crayon::red("error")` 返回带样式的字符串,`crayon::bold` 等可自由组合(`bold(red(...))`),`make_style("#ff8000")` 工厂式构造 truecolor 样式函数。现代 hadley/r-lib 配套 **`cli`** 在 crayon 原语之上添加语义化辅助(`cli_alert_danger()`、`cli_h1()`、复数化内联标记)。**`glue`** 在 `glue_col("{red error}")` 中模板化同样的词汇。处理 ANSI 感知的字符串操作(R 中标准的日志清洗器在此),**`fansi::strip_sgr`** 干净地移除每一条 CSI 序列,`fansi::nchar_ctl`/`substr_ctl` 在不破坏字节的前提下统计显示宽度并切片。 R 中的能力门控:`crayon::has_color()` 是权威调用 —— 它已经考虑了 `NO_COLOR`、`isatty(stdout())`、`crayon.enabled` 选项,以及 **RStudio Source 面板 vs Console 面板的差异**(「为什么我的 crayon 输出显示成原始转义码」最常见的 SERP 点击源 —— `source()` 运行走另一条剥离 ANSI 的输出通道;Console 面板正确解析)。knitr / rmarkdown 渲染也默认关闭颜色以保持输出可移植。
推荐库
- crayon
事实上的 R ANSI 库,被 tidyverse、testthat、pkgdown、devtools 采用。`red()`、`bold()`、`bgYellow()`、`underline()` —— 可组合的字符串函数(`bold(red("x"))`)。`make_style("#ff8000")` 工厂式构造 truecolor 样式器;`combine_styles()` 构造复合样式。`has_color()` 是标准能力检测(尊重 NO_COLOR、isatty、RStudio Source 面板)。`crayon::strip_style()` 从带 crayon 样式的字符串中移除 ANSI。
- cli
现代 hadley/r-lib 包 —— 在 crayon 之上构建的语义化辅助:`cli_alert_danger()`、`cli_alert_warning()`、`cli_alert_success()`、`cli_h1()`/`cli_h2()`、`cli_li()` 列表、`cli_progress_bar()` 进度条、复数化内联标记(`{?s/are}`)、可定制主题。需要一致用户体验、不想手工逐次调用样式函数时选它。
- glue
crayon 的字符串插值伴侣。`glue("x={x}")` 用于普通模板;`glue_col("{red error} at {green file}")` 解析内联的 crayon 样式名并应用对应转义。当周围字符串包含大量插值时,比连串 crayon 调用更整洁。
- fansi
ANSI 感知的字符串操作伴侣。`strip_sgr(s)` 是 R 中标准的日志清洗器 —— 干净处理每一条 CSI 序列,包括 256 色与 truecolor。`nchar_ctl(s)` 返回显示宽度(跳过转义字节,统计东亚宽字符);`substr_ctl(s, 1, n)` 切片时不会撕开正在进行的转义序列。当 crayon 的 `strip_style()` 范围过窄时选它。
常用写法
# R supports \033 (octal) and \u{1b} (Unicode) directly in
# double-quoted strings. \x1b (hex) ALSO works in modern R
# (≥ 4.0) but with a strict two-hex-digit constraint. \033
# is the portable form — works on every R back to 1.0.
# Single-quoted strings expand backslash escapes identically
# in R (no Perl/PHP-style quote-style difference).
cat("\033[1;31merror:\033[0m permission denied\n")
cat("\033[33mwarn:\033[0m deprecated flag\n")
cat("\033[32mok:\033[0m 142 tests passed\n")
# Truecolor — 38;2;R;G;B
cat("\033[38;2;255;128;0morange truecolor\033[0m\n")
# message() goes to stderr — useful for logs that should
# survive stdout redirection. print() and cat() go to stdout.
message("\033[33mdeprecated:\033[0m use foo() instead")# install.packages("crayon")
library(crayon)
# Each style is a function — call it with the text to wrap:
cat(red("error: "), "permission denied\n", sep = "")
cat(yellow("warn: "), "deprecated flag\n", sep = "")
cat(green("ok: "), "142 tests passed\n", sep = "")
# Composition is just function composition — bold + red:
cat(bold(red("FATAL")), " server crashed\n", sep = "")
cat(bold(underline(blue("link"))), "\n", sep = "")
# Truecolor via make_style — pass a hex or rgb triple:
orange <- make_style("#ff8000")
violet <- make_style(rgb(0.5, 0.2, 0.8))
cat(orange("warm orange"), " ", violet("violet"), "\n", sep = "")
# Background colours: bgRed, bgYellow, bgBlue, bgGreen, ...
cat(bgYellow(black(" CAUTION ")), " brakes wet\n", sep = "")
# combine_styles() builds a reusable compound:
heading <- combine_styles(bold, underline, blue)
cat(heading("Section 1"), "\n", sep = "")# crayon::has_color() is the canonical answer:
# TRUE → terminal R, RStudio Console pane,
# VSCode R extension, vim + nvim-R
# FALSE → RStudio Source pane (source() output sink
# strips ANSI bytes — the #1 "my colours
# disappeared" surprise on R), Rscript with
# piped stdout, R BATCH, knitr / rmarkdown
# rendering (disabled to keep output portable).
#
# crayon ALREADY respects NO_COLOR, isatty, the
# crayon.enabled option, and RStudio detection — call it
# as-is. Only roll your own when you don't want crayon
# as a hard dependency:
ansi_capable <- function() {
if (nzchar(Sys.getenv("NO_COLOR"))) return(FALSE)
if (!interactive()) return(FALSE)
if (!isatty(stdout())) return(FALSE)
TRUE
}
style <- function(text, sgr) {
if (!ansi_capable()) return(text)
sprintf("\033[%sm%s\033[0m", sgr, text)
}
cat(style("OK", "32"), "\n", sep = "")
cat(style("FAIL", "1;31"), "\n", sep = "")
# Force-disable globally (useful in CRAN checks / CI):
options(crayon.enabled = FALSE)
# Force-enable when crayon's detection guesses wrong
# (e.g. piping to less -R, which DOES render ANSI):
options(crayon.enabled = TRUE)# install.packages("fansi")
library(fansi)
# Canonical R strip-ANSI — handles every CSI sequence
# including the harder-to-strip 256-colour / truecolor
# forms and cursor-control bytes that simple regexes miss.
dirty <- paste0(
"\033[1;31mERROR\033[0m at line 42 ",
"\033[38;2;255;128;0m(slow)\033[0m"
)
cat(strip_sgr(dirty), "\n", sep = "")
# → "ERROR at line 42 (slow)"
# Pure base-R regex equivalent (no fansi dep) — close
# enough for common SGR / cursor / erase forms:
gsub("\033\\[[0-9;]*[mGKHJABCDsuhl]", "", dirty)
# ANSI-aware substring — base substr() would slice
# inside an escape sequence and corrupt the bytes;
# substr_ctl() only counts visible characters:
substr_ctl(dirty, 1, 5) # → "\033[1;31mERROR\033[0m"
# Display width respecting ANSI bytes (and East Asian
# wide chars) — crucial when laying out terminal tables:
nchar_ctl(dirty) # → 18 (not 49)
nchar_ctl("\033[1;31mERROR\033[0m") # → 5 (not 13)
# Round-trip a whole log file:
strip_log <- function(path_in, path_out) {
lines <- readLines(path_in, warn = FALSE)
writeLines(strip_sgr(lines), path_out)
}
# strip_log("app.log", "app.clean.log")