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.
Table of Contents
- 1. Why raw ANSI escape codes are fragile in scripts
- 2. tput basics: setaf, bold and sgr0
- 3. Checking terminal capabilities before output: tput colors
- 4. The isatty check: [ -t 1 ] and when output is not a terminal
- 5. Automatically disabling color when output is redirected to a file
- 6. Building a reusable color function for the whole script
- 7. The NO_COLOR standard and other conventions
- 8. Pitfalls: tput in subshells, missing terminfo and CI environments
- 9. tput compared to raw ANSI codes
- 10. Summary
- 11. FAQ
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.