Skip to main content
ansicode
R

ANSI escape codes in R — \033, crayon, fansi::strip_sgr

R supports \033 (octal) directly in double-quoted strings — the most portable ESC literal across every R version since the language's birth. cat("\033[31mred\033[0m\n") works on every interpreter, on macOS, Linux, BSDs, and modern Windows (Windows Terminal / Conhost 1709+ parses ANSI natively). Modern R (≥ 4.0) also accepts "\u{1b}" Unicode and "\x1b" hex (strict 2-digit) escapes. For the canonical helper reach for crayon — the de-facto R ANSI library used by tidyverse, testthat, pkgdown, devtools: crayon::red("error") returns a styled string, crayon::bold and friends compose freely (bold(red(...))), and make_style("#ff8000") factory-builds a truecolor styler. The modern hadley/r-lib companion cli adds semantic helpers (cli_alert_danger(), cli_h1(), pluralized inline markup) on top of crayon's primitives. glue templates the same vocabulary in glue_col("{red error}"). For ANSI-aware string operations (the canonical R log scrubber lives here), fansi::strip_sgr removes every CSI sequence cleanly, and fansi::nchar_ctl/substr_ctl count display width and slice without corrupting bytes. Capability gating in R: crayon::has_color() is the authoritative call — it already respects NO_COLOR, isatty(stdout()), the crayon.enabled option, and the RStudio Source-pane vs Console divergence (the #1 "why does my crayon output show raw escapes" SERP click — source() runs go through a different sink that strips ANSI; the Console pane parses it correctly). knitr / rmarkdown rendering also disables colour by default to keep the rendered output portable.

Recommended libraries

  • crayon

    De-facto R ANSI library used by tidyverse, testthat, pkgdown, devtools. red(), bold(), bgYellow(), underline() — composable string functions (bold(red("x"))). make_style("#ff8000") factory-builds truecolor stylers; combine_styles() builds compound styles. has_color() is the canonical capability check (respects NO_COLOR, isatty, RStudio Source pane). crayon::strip_style() removes ANSI from a crayon-styled string.

  • cli

    Modern hadley/r-lib package — semantic helpers built on crayon: cli_alert_danger(), cli_alert_warning(), cli_alert_success(), cli_h1()/cli_h2(), cli_li() lists, cli_progress_bar() progress, pluralized inline markup ({?s/are}), themable styles. The right call when you want consistent UX rather than hand-rolling each style call.

  • glue

    String-interpolation companion to crayon. glue("x={x}") for plain templating; glue_col("{red error} at {green file}") parses inline crayon style names and applies the escapes. Cleaner than concatenating crayon calls when the surrounding string has many interpolations.

  • fansi

    ANSI-aware string-ops companion. strip_sgr(s) is the canonical R log scrubber — handles every CSI sequence including 256-colour and truecolor cleanly. nchar_ctl(s) returns display width (skips escape bytes, counts East Asian wide chars); substr_ctl(s, 1, n) slices without corrupting an in-progress escape. The right call when crayon's strip_style() is too narrow.

Idiomatic patterns

Direct cat() with \033 in a double-quoted string
# 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")
crayon — canonical R ANSI library, composable styles
# 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 = "")
Capability gate — NO_COLOR + has_color() + RStudio caveat
# 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)
fansi::strip_sgr — canonical R log scrubber + ANSI-aware string ops
# 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")

Related sequences

Other languages