LESS colours — coloured man pages via LESS_TERMCAP_*, LESS=R, MANPAGER
`less` reads `LESS_TERMCAP_*` environment variables to colourise the bold / underline / standout sequences it sees from `groff` overstrike — that is the canonical recipe for coloured `man` pages. The other half is `LESS=R` (or `LESS='-R -F -X'`) which lets SGR bytes already in the upstream pipe (from `git log --color=always`, `ls --color=always`, `grep --color=always`) pass THROUGH less to the terminal rather than being stripped. Seven copy-paste snippets below cover the full map: the canonical 6-var LESS_TERMCAP_* set, the LESS env flags, the MANPAGER + MANROFFOPT + GROFF_NO_SGR interaction, lesspipe / LESSOPEN preprocessing, bat as a colourised MANPAGER, diff-so-fancy / delta as supercharged git pagers, and per-shell wire-up.
Configuration snippets
LESS_TERMCAP_* — the canonical 6-var set that colourises man pages
`less` reads `LESS_TERMCAP_*` environment variables to **override** the terminfo entries it would normally use for bold / underline / standout. When `man` invokes `less` to page output, the nroff produced by `groff` uses backspace-overstrike (`b\bbo\bol\bld\bd` = bold) and underscore-overstrike (`_\bb_\bo_\bl_\bd` = italic / argument names) — `less` interprets these and emits the configured `LESS_TERMCAP_*` bytes. Six vars matter: `md` (start bold), `me` (end bold / blink / standout), `us` (start underline), `ue` (end underline), `so` (start standout — used for the status / prompt line and search matches), `se` (end standout). Optional `mb` (start blink) is usually copied from `md` so blinking does not get hidden. The values are RAW ANSI / VT escape bytes — write the literal `\e[01;31m` form in your shell config.
# The canonical 6-var set — drop into ~/.bashrc / ~/.zshrc verbatim.
# Bold red for keywords / commands (md), green underline for arguments
# (us), yellow standout for status line + search matches (so).
export LESS_TERMCAP_md=$'\e[01;31m' # start bold (man: keywords)
export LESS_TERMCAP_me=$'\e[0m' # end bold / blink / standout
export LESS_TERMCAP_us=$'\e[01;32m' # start underline (man: args / files)
export LESS_TERMCAP_ue=$'\e[0m' # end underline
export LESS_TERMCAP_so=$'\e[01;44;33m' # start standout (status / search hit)
export LESS_TERMCAP_se=$'\e[0m' # end standout
export LESS_TERMCAP_mb=$'\e[01;31m' # start blink — copy md so it stays visible
# Verify they are exported with the right bytes (decode the ESC):
env | grep LESS_TERMCAP | cat -v
# should show ^[[01;31m etc — the ^[ is ESC (0x1b), the rest is the SGR
# See the effect immediately — load a man page:
man ls
# Quick a/b test — unset, reload, compare:
( unset LESS_TERMCAP_md LESS_TERMCAP_me LESS_TERMCAP_us \
LESS_TERMCAP_ue LESS_TERMCAP_so LESS_TERMCAP_se; \
man ls )
# vs the current shell session with the vars set.
# Note the $'...' syntax — bash / zsh ANSI-C quoting that turns \e into
# the literal ESC byte. If your shell does NOT support $'...' (older sh /
# dash) use printf:
# LESS_TERMCAP_md=$(printf '\e[01;31m'); export LESS_TERMCAP_mdLESS=R — colour passthrough when upstream emits raw SGR directly
`LESS_TERMCAP_*` covers the case where `less` itself adds colour by INTERPRETING the input. The other case is when upstream **already emits SGR bytes** (e.g. `git log --color=always`, `ls --color=always`, `grep --color=always`, `colordiff`, `diff-so-fancy`) — those bytes need to pass THROUGH `less` to the terminal. By default `less` strips control characters and shows `^[[31m` literal noise instead. The fix is the `-R` flag (or `--RAW-CONTROL-CHARS`), which lets SGR bytes through but still cleans other control characters. The `LESS` env var sets default flags applied to every `less` invocation — `LESS=R` makes raw-control-chars the global default. Modern `less` (since v530, 2017) also supports `-r` (lowercase) which passes EVERY control char unchanged — use `-R` not `-r` unless you specifically need other control chars rendered.
# Set the default once — every less invocation gains -R:
export LESS=R
# Optional companion flags many users add to LESS:
export LESS='-R -F -X'
# -R keep SGR bytes (already covered above)
# -F quit if output fits one screen (skip the pager-trap on short files)
# -X skip the alt-screen restore (so scrollback retains the pager content)
# These are the same flags git auto-sets when invoking its DEFAULT pager.
# Custom pagers (core.pager, PAGER, MANPAGER) do NOT inherit them — set
# LESS globally instead.
# One-shot — no env, flags on the command line:
git log --color=always --oneline | less -R
grep --color=always -rn TODO src/ | less -R
ls --color=always -la | less -R
# Without -R (the default), you see literal noise:
ls --color=always | less
# src/ app/ ESC[01;34mlib^[[0mESC[m ... (ugly)
# With -R, colour passes through:
ls --color=always | less -R
# src/ app/ (bold blue lib) ... (correct)
# Verify LESS is honoured by the binary:
echo "$LESS"
less --help 2>&1 | grep -E 'RAW|environment' | headMANPAGER vs PAGER — modern man honours both, but MANPAGER wins
`man` looks up its pager in this order: `$MANPAGER`, then `$PAGER`, then the compile-time default (typically `less`). Setting `MANPAGER` lets you give `man` a different pager than the one you use for everything else — useful when you want `man` to always force colour-rendering flags but leave plain `less` invocations alone. The canonical safe value is `MANPAGER='less -R'`. On distros where `groff` is recent enough (1.18.1+, 2008) and `MANROFFOPT=-c` is set, `groff` emits SGR directly INSTEAD of backspace-overstrike — at that point `LESS_TERMCAP_*` stops applying (less is no longer interpreting overstrike), and you need `MANPAGER='less -R'` for the SGR to pass through. Modern Debian / Ubuntu / Arch ship this configuration by default; the BSDs and older RHEL still ship the overstrike path. Set both — `MANPAGER='less -R'` AND the `LESS_TERMCAP_*` vars — to cover both paths transparently.
# Set both — covers groff-SGR path AND backspace-overstrike path:
export MANPAGER='less -R'
export MANROFFOPT='-c' # tell groff to emit SGR (modern groff)
# Plus the LESS_TERMCAP_* vars from snippet 1 (for the overstrike path).
# Check which path your distro uses — run man on any page, then in less
# press 'v' to drop into $EDITOR (or use !cat) and look at the raw bytes:
#
# If you see "b\bbo\bol\bld" overstrike → backspace-overstrike path,
# LESS_TERMCAP_* is in play.
# If you see "\x1b[1mbold\x1b[0m" → groff-SGR path,
# MANPAGER='less -R' is what matters.
# Force the overstrike path even on a modern distro (so LESS_TERMCAP_*
# applies again — useful when debugging custom palettes):
export GROFF_NO_SGR=1
# Test:
man ls
# Try changing LESS_TERMCAP_md to a glaring colour and reload to confirm
# which path is active:
export LESS_TERMCAP_md=$'\e[01;45m' # purple background
man ls # if you see purple text → overstrike path; else → SGR path
# When piping man output (e.g. into grep / a file) the formatting is
# normally stripped because the pager is not a TTY. Preserve it with:
MAN_KEEP_FORMATTING=1 man ls | head -50 # keeps overstrike bytes
# Then strip later with col -b:
MAN_KEEP_FORMATTING=1 man ls | col -b | head -50 # plain textLESSOPEN / lesspipe — preprocess input before less sees it
`less` runs `$LESSOPEN` (and optionally `$LESSCLOSE`) on every input file to give the user a chance to transform the bytes before they reach the pager — most distros ship `lesspipe.sh` (Debian / Ubuntu) or `lesspipe.lua` (Arch) and wire it via `eval "$(lesspipe)"` in `/etc/profile.d/less.sh`. Out of the box this lets `less archive.tar.gz` show a directory listing, `less foo.pdf` show extracted text, `less foo.deb` show package metadata — much wider input support than `less` alone. The colour angle: `lesspipe` integrates with `source-highlight` (or `highlight`, `bat`, `pygmentize`) so `less foo.py` shows the Python source with syntax highlighting. Configure the highlighter via `LESSOPEN='| /usr/bin/src-hilite-lesspipe.sh %s'` and add `LESS=" -R"` (note leading space) to keep colour passthrough on.
# Default Debian / Ubuntu setup (already in /etc/profile.d/less.sh):
export LESSOPEN="| /usr/bin/lesspipe %s"
export LESSCLOSE="/usr/bin/lesspipe %s %s"
# Verify it's working — should show metadata not raw bytes:
less /usr/lib/x86_64-linux-gnu/libc.so.6 # ELF binary → strings dump
less /etc/os-release # text file → straight through
less ./some.tar.gz # archive → tar tvfz listing
# Add syntax highlighting for source code via source-highlight:
sudo apt install source-highlight # Debian / Ubuntu
brew install source-highlight # macOS
# Wire it as the pre-processor — note leading space in LESS:
export LESSOPEN="| /usr/bin/src-hilite-lesspipe.sh %s"
export LESS=" -R" # leading space prevents typoed
# binary name issues
# Test it:
less app/page.tsx # should show syntax-highlighted TS
less script.sh # should show syntax-highlighted bash
less /etc/nginx/nginx.conf # should show coloured nginx config
# Disable for one invocation (debugging):
LESSOPEN= less raw.log
less --no-lessopen raw.log # explicit flag, less v530+
# Combine with bat for richer highlighting (snippet 5 explores bat more):
export LESSOPEN="| /usr/bin/bat --color=always --paging=never %s"bat / batcat — a colourised less replacement with syntax highlighting
`bat` (by sharkdp — Rust) is a `cat` / `less` hybrid that adds syntax highlighting, line numbers, Git modification markers, and a paging mode that respects `less` keybindings (`bat -p` for non-paging). It works as a drop-in `cat` replacement (`alias cat=bat`) AND as a MANPAGER for colourised man pages with syntax-highlighted code blocks. On Debian / Ubuntu the binary is renamed `batcat` (an older unrelated `bat` package squats on the name) — Arch / Fedora / Homebrew ship the binary as `bat`. Themes via `bat --list-themes` (`OneHalfDark` / `Dracula` / `gruvbox-dark` / `Solarized (light)` / `ansi` for terminal-palette-only). `BAT_THEME` env sets the default; `BAT_PAGER='less -R'` overrides the pager. `--color={auto,always,never}` and `NO_COLOR` honoured.
# Install:
# Arch: pacman -S bat
# Debian/Ubuntu: apt install bat (binary is "batcat", not "bat")
# Fedora: dnf install bat
# macOS: brew install bat
# Debian / Ubuntu workaround — alias batcat back to bat:
alias bat='batcat'
# Use as a cat replacement (paging only when output exceeds terminal):
bat app/page.tsx # syntax-highlighted, line numbers
bat -p app/page.tsx # plain mode (no header, no pager)
bat -A binary.dat # show non-printable as escapes
# Use as MANPAGER for colourised man pages:
export MANPAGER="sh -c 'col -bx | bat -l man -p'"
# col -bx strip backspace-overstrike (otherwise bat sees garbage)
# bat -l man syntax = man (highlights options + section refs)
# bat -p plain mode (no header)
# Theme + style configuration:
export BAT_THEME='OneHalfDark' # any theme from --list-themes
export BAT_STYLE='numbers,changes,header' # opt-in column set
export BAT_PAGER='less -R' # override default pager
# Verify which binary, where the config file lives:
bat --version
bat --config-file # path; create + edit to persist
bat --generate-config-file # write a fully-commented template
# Pipe coloured input into bat (preserve SGR):
git log --color=always --oneline | bat --paging=always
diff -u old.txt new.txt | bat -l diff
# Use as drop-in cat (with caveats — bat exits with paging on long output):
alias cat='bat -p' # plain mode = no extra UIdiff-so-fancy / delta — supercharged coloured diff pagers
Two popular pager-replacements specifically for git / diff output, both of which assume `LESS=" -R"` (or `LESS=FRX`) is set. **`diff-so-fancy`** (by so-fancy — Perl) cleans up unified-diff output: removes the `+++ / ---` headers, replaces `@@` hunk markers with a thin separator, and recolours `+ / -` lines with brighter SGR. Wire it via `git config --global core.pager "diff-so-fancy | less --tabs=4 -RFX"`. **`delta`** (by dandavison — Rust) goes further: side-by-side mode, syntax-highlighted code, configurable line numbers + plus/minus markers, Git-blame integration. Wire it via `git config --global core.pager delta` (delta auto-detects it is the pager and applies). `delta` reads its config from `~/.gitconfig` `[delta]` section — set `features = side-by-side line-numbers decorations` for the modern look. Both honour `NO_COLOR` and `--color=never`.
# Install:
# diff-so-fancy: brew install diff-so-fancy (or npm i -g diff-so-fancy)
# delta: brew install git-delta (or cargo install git-delta)
# diff-so-fancy — wire into git core.pager:
git config --global core.pager 'diff-so-fancy | less --tabs=4 -RFX'
git config --global interactive.diffFilter 'diff-so-fancy --patch'
# Optional colour tuning that diff-so-fancy assumes:
git config --global color.diff-highlight.oldNormal 'red bold'
git config --global color.diff-highlight.oldHighlight 'red bold 52'
git config --global color.diff-highlight.newNormal 'green bold'
git config --global color.diff-highlight.newHighlight 'green bold 22'
git config --global color.diff.meta 'yellow'
git config --global color.diff.frag 'magenta bold'
git config --global color.diff.commit 'yellow bold'
git config --global color.diff.old 'red bold'
git config --global color.diff.new 'green bold'
git config --global color.diff.whitespace 'red reverse'
# delta — wire it as the pager (auto-detects):
git config --global core.pager delta
git config --global interactive.diffFilter 'delta --color-only'
git config --global delta.navigate true
git config --global delta.light false
git config --global delta.line-numbers true
git config --global delta.side-by-side true
git config --global delta.syntax-theme 'OneHalfDark'
# Disable for one command:
git --no-pager diff
git -c core.pager=cat diff
# Test:
git diff HEAD~1
git log --oneline -p | head -50
# Pipe a raw diff (not via git) into either tool:
diff -u old.txt new.txt | diff-so-fancy | less -R
diff -u old.txt new.txt | deltaPer-shell wire-up — bash · zsh · fish · csh
The canonical setup combines all the snippets above: `LESS_TERMCAP_*` for man-page colour, `LESS='-R -F -X'` for SGR passthrough + sane quitting + scrollback retention, `MANPAGER='less -R'` to flow SGR from modern groff, optionally `BAT_THEME` for syntax-highlighted code. Each shell exports differently. The macOS bash double-trap from `/ls-colors` and `/grep-colors` applies here too — `~/.bashrc` only runs on non-login shells but Terminal.app spawns login shells, so env exports go in `~/.bash_profile` (or `~/.zprofile` for macOS default zsh). Fish needs the literal-ESC byte via the printf-into-set pattern because `$'\e'` ANSI-C quoting is bash / zsh syntax only.
# ~/.bashrc (Linux) or ~/.bash_profile (macOS Terminal.app):
export LESS_TERMCAP_md=$'\e[01;31m' # bold → bold red
export LESS_TERMCAP_me=$'\e[0m'
export LESS_TERMCAP_us=$'\e[01;32m' # underline → bold green
export LESS_TERMCAP_ue=$'\e[0m'
export LESS_TERMCAP_so=$'\e[01;44;33m' # standout → bold yellow-on-blue
export LESS_TERMCAP_se=$'\e[0m'
export LESS_TERMCAP_mb=$'\e[01;31m'
export LESS='-R -F -X'
export MANPAGER='less -R'
export MANROFFOPT='-c'
export PAGER='less'
# ~/.zshrc / ~/.zprofile (same syntax, $'...' works in zsh too):
export LESS_TERMCAP_md=$'\e[01;31m'
# ... (same six exports)
export LESS='-R -F -X'
export MANPAGER='less -R'
# ~/.config/fish/config.fish — fish has NO $'...' quoting, use printf:
set -gx LESS_TERMCAP_md (printf '\e[01;31m')
set -gx LESS_TERMCAP_me (printf '\e[0m')
set -gx LESS_TERMCAP_us (printf '\e[01;32m')
set -gx LESS_TERMCAP_ue (printf '\e[0m')
set -gx LESS_TERMCAP_so (printf '\e[01;44;33m')
set -gx LESS_TERMCAP_se (printf '\e[0m')
set -gx LESS_TERMCAP_mb (printf '\e[01;31m')
set -gx LESS '-R -F -X'
set -gx MANPAGER 'less -R'
# ~/.cshrc / ~/.tcshrc — printf inside backticks, setenv (no = sign):
setenv LESS_TERMCAP_md "`printf '\e[01;31m'`"
setenv LESS_TERMCAP_me "`printf '\e[0m'`"
# ... (same six setenv)
setenv LESS '-R -F -X'
setenv MANPAGER 'less -R'
# Verify:
man ls # should show coloured keywords + underlined args
echo "$LESS" # should print "-R -F -X"
env | grep -E 'LESS_TERMCAP|MANPAGER' | cat -v # show with ^[ visible
# macOS double-trap reminder — these only run on LOGIN shell:
# ~/.bash_profile, ~/.zprofile, ~/.profile
# These only run on NON-login (subshell) shell:
# ~/.bashrc, ~/.zshrcLESS_TERMCAP_* slot reference
The seven LESS_TERMCAP_* env vars — what each starts or ends, the paired start/end variable, and the role less assigns to it. The me reset is shared by md / mb / so; ue pairs only with us; se pairs only with so.
| Env var | Paired with | Meaning | Used for |
|---|---|---|---|
| LESS_TERMCAP_md | me | start bold | man-page keywords / command names / headings |
| LESS_TERMCAP_me | md/mb/so | end bold / blink / standout (single reset) | shared reset for the three start sequences |
| LESS_TERMCAP_us | ue | start underline (italic in man) | man-page argument names / file names / emphasis |
| LESS_TERMCAP_ue | us | end underline | paired reset for the underline start sequence |
| LESS_TERMCAP_so | se | start standout (inverted highlight) | status / prompt line at the bottom of less; search match highlights |
| LESS_TERMCAP_se | so | end standout | paired reset for the standout start sequence |
| LESS_TERMCAP_mb | me | start blink (rare) | rarely used; copy from md so blinking content stays visible |
Related references
Adjacent pages on this site that cover the surrounding ANSI / pager / man / diff ecosystem.
- Git colour configCompanion per-tool page — git's color.ui / per-command keys / slot palette / core.pager + LESS=FRX (the same LESS env this page covers).
- LS_COLORS env varCompanion per-tool page — LS_COLORS syntax, dircolors workflow, BSD LSCOLORS divergence, eza / exa.
- GREP_COLORS env varCompanion per-tool page — GREP_COLORS slot syntax, ripgrep --colors dotted syntax, ag --color-* flags, the pipe-into-grep trap.
- Common ANSI pitfallsSymptom → cause → fix for adjacent issues — less strips SGR by default, MANPAGER not honoured, groff-SGR vs overstrike path confusion.