Portable Terminal Color Output With tput and ANSI Codes
AI generated
$_
#!/
Bash · Terminal · tput · Output
Detecting Color and Terminal Capabilities
tput and ANSI codes done portably, without broken log files

Raw ANSI escape codes like \e[31m look fine in your own terminal, but break the moment a script runs on a different terminal type or its output gets redirected into a log file. tput instead queries the current terminal's actual capabilities and returns the matching escape sequence, while a simple isatty check makes color vanish automatically once nobody is reading it interactively anymore.

15 min read tput · setaf · isatty Bash 4.x · 5.x · POSIX terminals

1. Why raw ANSI escape codes are fragile in scripts

A hardcoded escape code like \e[31m for red assumes the target terminal understands exactly that sequence. That holds true for most modern terminals such as xterm or gnome-terminal, but not necessarily for older terminals, serial consoles, or minimal environments, where the exact feature set depends on the value of the TERM environment variable and can vary widely.

It gets even more problematic once a script's output no longer lands directly in a terminal but gets redirected into a file or piped to another program. Raw escape codes then show up as cryptic control characters right in the middle of the text, interfere with parsing by other tools, and make log files unnecessarily hard to read, because they get sent blindly regardless of whether a terminal is even watching.

2. tput basics: setaf, bold and sgr0

tput looks up the terminfo database to find which escape sequence actually produces the desired effect for the terminal type stored in $TERM, and prints exactly that sequence. tput setaf 1 returns the sequence for red as a foreground color, tput bold enables bold text, and tput sgr0 resets every text attribute back to its default, more reliably than a manually assembled reset code.

Since every tput call spawns its own subprocess, it pays off to store the needed sequences in variables once at the start of the script and reuse those variables as often as needed afterward, instead of calling tput again on every output line inside a loop. That saves a noticeable number of process spawns, especially in scripts with many colored log lines.


#!/usr/bin/env bash
set -euo pipefail

RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
BOLD=$(tput bold)
RESET=$(tput sgr0)

echo "${BOLD}${GREEN}Deployment successful${RESET}"
echo "${RED}Error: connection refused${RESET}"

3. Checking terminal capabilities before output: tput colors

tput colors returns the number of colors the current terminal supports according to its terminfo entry, usually 8, 16, or 256. A script offering colored output should check this value before the first setaf call, instead of blindly assuming color support, since calling with a color index higher than what a terminal supports can produce unexpected results.

A robust check pattern is if [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then, which covers both the case where tput colors returns a valid value below 8 and the case where the call fails outright, for example because no matching terminfo entry exists.


#!/usr/bin/env bash
set -euo pipefail

color_count="$(tput colors 2>/dev/null || echo 0)"

if [[ "$color_count" -ge 8 ]]; then
  echo "Terminal supports at least 8 colors ($color_count total)"
else
  echo "Terminal has no usable color support"
fi

4. The isatty check: [ -t 1 ] and when output is not a terminal

The expression [ -t 1 ] checks whether file descriptor 1, that is stdout, is connected to an actual terminal. If a script's output gets redirected into a file with > log.txt or piped to another program with |, this test returns false, because stdout is then no longer a terminal but a regular file or a pipe.

For reliable color output, tput colors and [ -t 1 ] belong together: escape sequences should only ever be sent when both conditions hold, the terminal supports color and the output is actually being read interactively. Either check alone is not enough to reliably avoid broken output in every situation.


#!/usr/bin/env bash
set -euo pipefail

supports_color() {
  [[ -t 1 ]] || return 1
  local n
  n="$(tput colors 2>/dev/null || echo 0)"
  [[ "$n" -ge 8 ]]
}

if supports_color; then
  echo "$(tput setaf 2)Color enabled$(tput sgr0)"
else
  echo "Color disabled (no TTY or no color support)"
fi

5. Automatically disabling color when output is redirected to a file

Instead of adding a color condition on every single output line, it is far easier to maintain by setting the color variables once at the start of the script, either to the real tput sequences or to empty strings, depending on whether [ -t 1 ] and tput colors succeed. The rest of the script code then uses the same variables like $RED and $RESET everywhere, without checking again whether color is active.

On top of the automatic detection, many users expect explicit override flags like --no-color or --color=always to control the automatic detection when needed, for example when a CI pipeline wants the output colored despite lacking a real terminal, because a downstream log viewer interprets the codes itself.


#!/usr/bin/env bash
set -euo pipefail

force_color=0
no_color=0

for arg in "$@"; do
  case "$arg" in
    --color=always) force_color=1 ;;
    --no-color) no_color=1 ;;
  esac
done

if [[ "$no_color" -eq 1 ]]; then
  use_color=0
elif [[ "$force_color" -eq 1 ]]; then
  use_color=1
elif [[ -t 1 ]] && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
  use_color=1
else
  use_color=0
fi

if [[ "$use_color" -eq 1 ]]; then
  RED=$(tput setaf 1); GREEN=$(tput setaf 2); RESET=$(tput sgr0)
else
  RED=""; GREEN=""; RESET=""
fi

echo "${GREEN}Ready${RESET}"

6. Building a reusable color function for the whole script

Instead of combining color variables directly on every output line, small wrapper functions like log_info() and log_error() bundle the complete formatting in a single place. The rest of the code just calls log_info "Deployment started", without touching escape sequences itself, which rules out typos in color names and forgotten RESET calls from the start.

With several deployment scripts in the same project, it is worth moving these functions into a single, commonly sourced file that every script includes with source at the top. That keeps the color scheme consistent across every script, and a later adjustment, say a different base tone, only needs to be maintained in one place.


#!/usr/bin/env bash
set -euo pipefail

log_info()  { echo "${GREEN}[INFO]${RESET} $*"; }
log_error() { echo "${RED}[ERROR]${RESET} $*" >&2; }

log_info "Starting deployment"
log_error "Configuration file not found"

7. The NO_COLOR standard and other conventions

The community convention NO_COLOR (see no-color.org) states that any set, non-empty value of this environment variable should disable all color output from a program, regardless of what the terminal would actually support. Many modern CLI tools respect this variable by now, which makes it worth honoring in your own Bash scripts too.

The sensible check order is: respect NO_COLOR first and disable color immediately when it is set, then evaluate explicit script flags like --no-color, and only after that let the automatic detection via [ -t 1 ] and tput colors take effect. That way, the more explicit user intent always wins over the automatic detection.


#!/usr/bin/env bash
set -euo pipefail

if [[ -n "${NO_COLOR:-}" ]]; then
  use_color=0
elif [[ -t 1 ]] && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
  use_color=1
else
  use_color=0
fi

echo "use_color=$use_color"

8. Pitfalls: tput in subshells, missing terminfo and CI environments

Minimal Docker base images often lack a complete terminfo database, which can make tput calls fail or print an error to stderr instead of simply returning an empty sequence. Every tput call in a portable script should therefore be guarded with 2>/dev/null and combined with a sensible fallback value such as || echo 0.

CI environments frequently set TERM to dumb or leave it unset entirely, which makes tput colors return a low or erroneous value. A script that does not guard against this could otherwise stop an entire pipeline with a hard error just because a purely cosmetic feature like colored output is unavailable, which is why the fallback path should always be tested.

9. tput compared to raw ANSI codes

The choice between raw ANSI codes and tput essentially comes down to a trade-off between minimal effort and real portability. For a throwaway script that only ever runs in your own terminal, hardcoded codes might be enough, but for any script that gets shared, deployed in CI pipelines, or written to a log file, the combination of tput, an isatty check, and NO_COLOR support is the more robust choice.

Approach Portability Terminal detection Recommendation
Hardcoded raw ANSI codes Low None Only for short, purely interactive throwaway scripts
tput without checks Medium No automatic disabling Interactive scripts with no redirection risk
tput + isatty + NO_COLOR High Complete, including user override Production-grade CLI tools and deployment scripts
Third-party color libraries High Depends on the library Large CLI projects with many output formats

Mironsoft

Shell automation, DevOps tooling and deployment infrastructure

Shell scripts that hold up in production?

We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.

Code Review

ShellCheck analysis and manual review for critical Bash pattern violations.

Refactoring

Retrofitting error handling, logging and safe file operations.

CI Integration

Wiring ShellCheck and BATS into pipelines and building regression tests.

10. Summary

tput and Color Output in Bash: The Essentials at a Glance

tput basics

tput setaf, tput bold and tput sgr0 query the actual terminfo capability instead of hardcoding escape codes.

Checking capabilities

tput colors returns the number of supported colors, guarded with 2>/dev/null || echo 0 against missing terminfo entries.

isatty check

[ -t 1 ] detects whether stdout goes to a real terminal, automatically disabling color when redirected to a file.

Respecting NO_COLOR

The NO_COLOR environment variable and explicit --no-color/--color=always flags take priority over automatic detection.

11. FAQ: tput and Color Output in Bash: The Essentials at a Glance

1Why should I use tput instead of raw ANSI codes?
tput queries the terminfo database for the actual terminal type stored in TERM and returns only the sequences that terminal truly understands, instead of blindly sending a fixed sequence.
2What does tput colors return?
The number of colors the current terminal supports according to terminfo, usually 8, 16, or 256. The value should be checked before the first setaf call.
3What does [ -t 1 ] do?
The test checks whether file descriptor 1, that is stdout, is connected to an actual terminal. It returns false when redirected into a file or a pipe.
4How do I automatically disable color when output is redirected to a file?
Set the color variables once at the start of the script depending on [ -t 1 ] and tput colors, either to real sequences or to empty strings, then use the same variables everywhere afterward.
5What is the NO_COLOR standard?
A community convention stating that any set, non-empty NO_COLOR should disable all color output from a program, regardless of the terminal's actual capability. Many CLI tools respect this variable.
6Why should I guard tput calls with 2>/dev/null?
Minimal environments without a complete terminfo database can make tput fail with an error message. 2>/dev/null combined with a fallback like || echo 0 prevents that message from disrupting the script.
7Why does tput colors often return 0 or an error in a CI pipeline?
CI environments frequently set TERM to dumb or leave it empty, so no matching terminfo entry with color support can be found.
8How do I build a reusable color function?
Small wrappers like log_info() and log_error() bundle the formatting in one place. The rest of the code just calls these functions, without working with escape sequences directly.
9Should I offer a --no-color flag on top of automatic detection?
Yes. Many users want to explicitly override the automatic detection, for example in log viewers that interpret escape codes themselves, or in environments without a reliable TERM value.
10In what order should I check NO_COLOR, my own flags and isatty?
Respect NO_COLOR first, then evaluate explicit script flags like --no-color or --color=always, and only after that let automatic detection via isatty and tput colors take effect.