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