Skip to main content
ansicode

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.

shell / env
# 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_md

LESS=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.

shell / env
# 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' | head

MANPAGER 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.

shell / env
# 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 text

LESSOPEN / 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.

shell / env
# 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.

shell / env
# 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 UI

diff-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.

shell / env
# 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 | delta

Per-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.

shell / env
# ~/.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, ~/.zshrc

LESS_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.

LESS_TERMCAP_* slot reference
Env varPaired withMeaningUsed for
LESS_TERMCAP_mdmestart boldman-page keywords / command names / headings
LESS_TERMCAP_memd/mb/soend bold / blink / standout (single reset)shared reset for the three start sequences
LESS_TERMCAP_usuestart underline (italic in man)man-page argument names / file names / emphasis
LESS_TERMCAP_ueusend underlinepaired reset for the underline start sequence
LESS_TERMCAP_sosestart standout (inverted highlight)status / prompt line at the bottom of less; search match highlights
LESS_TERMCAP_sesoend standoutpaired reset for the standout start sequence
LESS_TERMCAP_mbmestart blink (rare)rarely used; copy from md so blinking content stays visible

Adjacent pages on this site that cover the surrounding ANSI / pager / man / diff ecosystem.