How and Why You Should Add Color to Your Scripts

There’s probably no Unix tool more useful than the venerable shell for automating tasks. Your scripts can become even more useful when you add color to their output to help humans pick out important information.

If you’ve added color to your scripts before, you may have used ANSI escape codes. The problem is that, when they’re not being used in a context that understands them, those codes can make output unreadable. Thankfully, this is a very solvable problem.

How Terminal Colors Work

Colorizing and emphasizing terminal output goes way back. ANSI escape codes emerged in the 1970s as a standard way to do this — before then, different terminals all had different ways of doing things.

ANSI escape codes work by adding a special control code, beginning with an ESC character (often seen as “^[“), in line with the text. For example, changing the text color to red would be ESC followed by the string “[31m”.

The problem is that not everything that displays or captures the output of a command understands ANSI escape codes. If you’ve ever seen output littered with something like “^[[1mprocessing ^[[31msrc^[(B^[[m…”, you’ve experienced this.

What we need is a way to apply color selectively. It should be useful to humans scanning large volumes of output without corrupting output where color isn’t supported.

Enter tput

The “tput” command is a standard part of Unix and abstracts away a lot about terminal control. For example, if you want to switch colors to red:

tput setaf 1; echo red

Or use boldface:

tput bold; echo bold

And if you want to reset all the colors and such back to normal:

tput sgr0

These commands respect terminal settings (e.g. the “TERM” environment variable) and will output the correct control sequences depending on what terminal it thinks you’re using. The terminfo database that’s part of Unix systems knows about all kinds of terminals and all kinds of options and is consulted to make this happen.

But even tput doesn’t handle everything. For example, this code will create a file called “red.txt” that has the red control characters for the current terminal inside:

(tput setaf 1; echo red) >red.txt

Thus, we need a way to turn this off if we’re not actually outputting to terminals.

Teletypes and environment variables

Unix has the concept of a “tty”, short for “teletype”. Determining if we’re using a tty is our first step—if you just run a script, it’ll be outputting to a tty; if you redirect it, it won’t be.

This code uses the “test” shell command’s “-t” flag, which tests a file descriptor (“1” in this case for standard output) to see if it’s a tty or not:

test -t 1 && echo "we're a tty!" || echo "we're not a tty!"

This handles many cases already, but sometimes you do want to capture color if you’re not on a tty. Or maybe you want to just turn color off entirely, whether it’s a tty or not.

Turning color off is probably the most standardized: use the “NO_COLOR” environment variable. Not everyone supports it, but lots of projects do — so respecting it ourselves is also a good idea.

Forcing it on seems a bit less a safe bet, but “CLICOLOR_FORCE” seems like a good bet. Of course, if we respect “CLICOLOR_FORCE”, we should also respect “CLICOLOR” when set to “0” and interpret that as “don’t use color”.

In conclusion, and accounting for the out-there case that we can’t find “tput”, the somewhat-hairy logic looks like this:

  1. If “CLICOLOR_FORCE” is set to anything but “0”, enable color.
  2. If “NO_COLOR” is set, or “CLICOLOR” is set to “0”, disable color.
  3. If output is a tty, enable color.

We can implement this by setting a variable, “TPUT”, to either:

  1. the tput binary (if wanted, present, and executable), or
  2. “true”, a shell command that always returns a success variable,

like this:

color_on() {
    test ! -z "${CLICOLOR_FORCE}" -a "${CLICOLOR_FORCE}" != 0 && return 0
    test "${CLICOLOR}" = "0" -o ! -z "${NO_COLOR}" && return 1
    test -t 1 && return 0
    return 1
TPUT=$(which tput)
test -x "${TPUT}" && color_on || TPUT=true

Now, whenever we dereference and run “${TPUT}”, we’ll get the terminal sequence if we want it, or just nothing if we don’t.

Wrapping Up Our Text

Whew! Now, the rest is fairly straightforward.

First, we don’t want any tput errors to derail us (for example, if we use an unknown capability). So let’s wrap it in a function to discard errors and ignore error exits:

tput() {
    "${TPUT}" $@ 2>/dev/null || true

Now, a building block: a function that turns on what we want, outputs our text, then resets all text settings:

tput_wrap() {
    echo $(tput ${capability})$@$(tput sgr0)

Using this, bold text is super easy:

bold() {
    echo $(tput_wrap bold $@)

To set colors, let’s make one more building block that uses “setaf” for us:

setaf() {
    echo $(tput_wrap "setaf ${arg}" $@)

And now the rest of the color functions are straightforward. For example, red:

red() {
    setaf 1 $@

Other colors are just changing the name and the number.

Color in Action

At last, we have our color output tools, which we can use without thinking about settings or the terminal or whether we’re being redirected to a file or anything else:

echo $(bold processing $(red src))...

Now, there is one catch. In the interest of simplicity, we’ve implemented these to always completely reset at the end of “tput_wrap”. This means, that, while you can stack multiple items, you can’t wrap several different colors in “bold”, for example, because you’re resetting when you leave the inner color. This was an intentional design choice for now, but if you’ve got an awesome idea to patch that up, I’m all ears.

As far as using the code, you can copy-paste the parts you need from my GitHub repository right into your script. You could also put it into a library script in your project that you can source if you have many scripts that use it. The included example shows just that.

I’ve also only implemented the seven basic colors in the script, as well as bold. I’ve found this satisfies everything I need. You can, of course, add more; but keep in mind how visible (or not) your color choices might be in, for example, light color schemes.

No matter what you do, I hope this has taught you some interesting things about how terminals work, as well as how to make sure your software works in as many places as it possibly can.

  • Brian Oxley says:

    I rather appreciate this post — it is near and dear to my heart as I encourage colleagues to use the command line more, and webapps less.

    I like your implementation, but for myself I leaned move on pre-defined variables with a function to support a wider color palette. I’ve not found much use for cursor control or other advanced ANSI features.


  • Join the conversation

    Your email address will not be published. Required fields are marked *