Skip to main content
ansicode

ANSI escape codes in your shell

Five shells, five syntaxes for getting an ESC byte onto stdout — covered side by side. Bash and Zsh have ANSI-C quoting ($'\e[…]') plus printf; Zsh adds prompt-only %F{red} shortcuts; Fish does without escape literals entirely and exposes set_color; PowerShell 7+ ships $PSStyle but 5.1 still needs [char]27; cmd.exe relies on host-level ConPTY support and has no clean batch idiom for ESC. Each shell has the working snippet, the one gotcha you'll hit, and links to the underlying sequence.

Bash

ANSI escape codes in Bash

Bash gives you three reliable ways to emit an ESC byte: printf with octal/hex escapes (POSIX-portable, the safest choice in scripts that may also run under dash or sh); ANSI-C quoting $'\e[...]' (bash + zsh only, but by far the most readable); and tput (terminfo-aware, the safest across $TERM values). Avoid echo -e — it works in bash but not in dash / sh, so portable scripts that call it break silently on Debian /bin/sh.

Idioms

POSIX-portable printf

Works in every POSIX shell — bash, dash, ash, ksh, mksh, busybox sh. Use this in scripts whose shebang is #!/bin/sh.

printf '\033[1;31m%s\033[0m %s\n' 'error:' 'permission denied'
ANSI-C quoting ($'…')

bash and zsh only — but unmatched readability. \e expands to a literal ESC byte inside $'…' but NOT inside plain '…' or "…".

red=$'\e[31m'
bold=$'\e[1m'
reset=$'\e[0m'
echo "${bold}${red}error:${reset} permission denied"
Terminfo via tput

tput resolves cap names against the current $TERM, so the same script degrades cleanly on dumb / 16-colour / 256-colour terminals.

RED=$(tput setaf 1)
BOLD=$(tput bold)
RESET=$(tput sgr0)
printf '%s%serror:%s permission denied\n' "$BOLD" "$RED" "$RESET"
echo -e (avoid in portable scripts)

Bash interprets backslash escapes only when -e is passed. dash / posix-mode sh print -e as a literal flag and the escapes stay un-expanded; printf or $'…' is the safe replacement.

# Works in bash …
echo -e '\033[31merror\033[0m'
# … breaks under dash / sh — outputs literal: -e \033[31merror\033[0m
Zsh

ANSI escape codes in Zsh

Zsh inherits bash's ANSI-C quoting ($'\e[…]') and adds prompt expansion shortcuts — %F{red}…%f for foreground colour, %B…%b for bold, %K{blue}…%k for background — which only expand inside $PS1 / $PROMPT / $RPROMPT, or after print -P. Outside prompts, treat zsh like bash: $'\e[31m' or printf '\033[31m…' both work. The autoload colors module exposes named arrays $fg / $bg / $reset_color for a less cryptic style.

Idioms

Prompt expansion in $PS1

%F{name}…%f / %B…%b / %K{name}…%k expand only inside prompts. Wrap them with %{…%} only if you mix in raw \e codes whose width zsh can't compute.

# In ~/.zshrc
PS1='%B%F{green}%n@%m%f%b:%F{blue}%~%f%# '
print -P expands prompt codes outside $PS1

Same %F{red}…%f shorthand, anywhere a script wants colour without spelling out the SGR bytes.

print -P '%F{red}error:%f permission denied'
Raw ESC via ANSI-C quoting

When you want full SGR control (truecolor, OSC, etc.) prompt codes don't reach — fall back to literal escapes.

local red=$'\e[38;2;255;120;120m'
local reset=$'\e[0m'
echo "${red}truecolor red${reset}"
autoload colors — named SGR arrays

Loads $fg / $fg_bold / $bg / $reset_color associative arrays so scripts read as English, not SGR numbers.

autoload -U colors && colors
echo "${fg[red]}error:${reset_color} permission denied"
Fish

ANSI escape codes in Fish

Fish doesn't ship $'…' or \e expansion — neither single nor double quotes interpret backslash escapes. The native answer is set_color, a builtin that takes colour names (red, blue, brmagenta) or 24-bit hex (cba6f7), with --bold / --italic / --underline / --background as flags. For the rare case you need a raw byte (OSC, DCS, etc.), call printf '\e[…]'.

Idioms

set_color — the native idiom

Pipe the colour change directly; reset with set_color normal. No escape literals in your script source.

set_color red
echo "error: permission denied"
set_color normal
Inline with begin/end or command substitution

Mid-line colour changes need set_color output captured into the echo argument.

echo (set_color red)"error:"(set_color normal)" permission denied"
Truecolor (24-bit) hex

Hex argument emits SGR 38;2;R;G;B under the hood. Pair with --background for full fg + bg control.

set_color cba6f7 --bold
echo "lavender bold"
set_color normal
Raw ESC for OSC / DCS / non-SGR codes

Fall through to printf when set_color doesn't cover the byte you need (window title, hyperlinks, alt screen).

# Set window title via OSC 0
printf '\e]0;%s\a' "$argv[1]"
PowerShell

ANSI escape codes in PowerShell

PowerShell 7.2+ ships $PSStyle — a built-in object exposing every SGR colour and attribute as a property ($PSStyle.Foreground.Red, $PSStyle.Bold, $PSStyle.Reset). It emits ANSI under the hood and honours $PSStyle.OutputRendering for redirection-safe output. On Windows PowerShell 5.1 (still shipped with Windows 10/11), $PSStyle doesn't exist; the canonical workaround is [char]27 to materialise an ESC byte, or `e (backtick e) in double-quoted strings from PS 6+. Write-Host -ForegroundColor uses the host's console API, NOT ANSI, so it doesn't compose with $PSStyle codes and gets stripped on redirect.

Idioms

$PSStyle (PowerShell 7.2+)

The modern, semantic API — auto-resets, redirection-aware, composable. Prefer this in new code.

"$($PSStyle.Bold)$($PSStyle.Foreground.Red)error:$($PSStyle.Reset) permission denied"
[char]27 (Windows PowerShell 5.1 and older)

ESC is decimal 27. Materialise it once into $e, then build escape sequences as ordinary strings.

$e = [char]27
"$e[1;31merror:$e[0m permission denied"
`e in double-quoted strings (PowerShell 6+)

Backtick e is PowerShell's ESC escape — the cleanest spelling when $PSStyle isn't an option.

"`e[1;31merror:`e[0m permission denied"
$PSStyle.OutputRendering — strip ANSI when piped

Default Host emits ANSI to a terminal but strips it from redirected / piped output. Set to Ansi to force, PlainText to suppress.

$PSStyle.OutputRendering = 'Ansi'      # always emit, even when redirected
$PSStyle.OutputRendering = 'PlainText' # never emit
$PSStyle.OutputRendering = 'Host'      # default: terminal yes, redirect no
cmd.exe (batch)

ANSI escape codes in cmd.exe / batch

cmd.exe gained ANSI support in Windows 10 build 14393 (Anniversary Update, 2016) and it's enabled by default in any ConPTY-aware host — Windows Terminal, VS Code's terminal, Cursor, Hyper. The legacy conhost.exe window opened by Win+R cmd may or may not have ANSI on, depending on Windows version and a per-window VT-processing flag. There is no clean batch syntax for an ESC byte: you embed a literal 0x1b character into your .bat file via an editor that can save it, or you reach for the for /f prompt $H trick. Realistically, for anything beyond a one-off, call PowerShell from your batch (powershell -NoProfile -Command "…") instead.

Idioms

Embed literal ESC via for /f + prompt $H

Black-magic batch idiom — $H is prompt's backspace token, the for /f loop captures a single byte that is then re-purposed as ESC after a trim. Sourced widely from Microsoft's own ConPTY samples.

@echo off
for /f %%a in ('echo prompt $E ^| cmd') do set "ESC=%%a"
echo %ESC%[1;31merror:%ESC%[0m permission denied
UTF-8 code page first — chcp 65001

Independent of ANSI, but almost always wanted alongside it: chcp 65001 makes cmd interpret your script and output as UTF-8 (default is OEM 437 / 936 / etc.).

@echo off
chcp 65001 > nul
echo Multilingual: αβγ • 中文 • emoji 🚀
Delegate to PowerShell for non-trivial styling

Once you need more than one or two coloured lines, calling PowerShell is shorter, more readable, and works identically on every Windows host.

@echo off
powershell -NoProfile -Command "Write-Host \"$([char]27)[1;31merror:$([char]27)[0m permission denied\""

See also

/use covers the library layer (chalk, fatih/color, rich, ncurses). This page is the shell-syntax layer — how to spell an escape in your dotfile. terminfo maps tput capabilities to the bytes those shells end up sending; pitfalls catches everything that survives the syntax but breaks at runtime.