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