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.
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
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'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"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"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[0mANSI 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
%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%# 'Same %F{red}…%f shorthand, anywhere a script wants colour without spelling out the SGR bytes.
print -P '%F{red}error:%f permission denied'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}"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"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
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 normalMid-line colour changes need set_color output captured into the echo argument.
echo (set_color red)"error:"(set_color normal)" permission denied"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 normalFall 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]"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
The modern, semantic API — auto-resets, redirection-aware, composable. Prefer this in new code.
"$($PSStyle.Bold)$($PSStyle.Foreground.Red)error:$($PSStyle.Reset) permission denied"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"Backtick e is PowerShell's ESC escape — the cleanest spelling when $PSStyle isn't an option.
"`e[1;31merror:`e[0m permission denied"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 noANSI 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
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 deniedIndependent 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 🚀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.