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() {
    capability=$1
    shift
    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() {
    arg=$1
    shift
    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.

Conversation
  • 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.

    ref: https://github.com/binkley/shell/tree/master/color

  • Chris says:

    Mattie, your description of disabling color output by setting CLICOLOR to 0 is not universal in programs that recognize CLICOLOR. For example, the ls utility in FreeBSD and macOS does not behave that way; it enables color output when CLICOLOR is set to any value at all, even if that value is either the digit 0 or the empty string.

    • Yeah, there are definitely some… fun edge cases.

      However, I do think that the case of someone setting “CLICOLOR” to “0” and then expecting color should be rather rare. “0” as an environment variable value is fairly well-understood to mean “off”, so I would argue that ls not treating that properly is a problem with how ls implements it, and most people wouldn’t do that in the first place.

      In short, if someone has it set to “0” but actually wanted color, they should change it to “1”.

  • Chris says:

    Mattie, the same argument could be made for other environment variable values — someone setting CLICOLOR to “off”, or “no”, or “N”, or “false”, or “F”, &c. might also expect color output to be disabled by those values, but such expectations would be unmet.

    “Properly” is in the eye of the beholder. If not for the well-defined behavior of NO_COLOR at https://no-color.org/, someone might think that setting NO_COLOR to “0” would disable the disabling of color output — but it doesn’t. Setting NO_COLOR to “0”, to its putative opposite “1”, or to any other non-empty value disables color output in programs that recognize NO_COLOR.

    I’d guess that the Berkeley ls utility (which both FreeBSD and macOS use) is the most used program with CLICOLOR recognition, and it has always used the mere presence of CLICOLOR in the environment to enable color output, no matter what value it has. The ls utility in FreeBSD also recognizes the COLORTERM environment variable (which predates CLICOLOR), and its behavior is that any value except the empty string enables color output. The behavior of NO_COLOR was undoubtedly based upon the behavior of COLORTERM.

    In summary, if someone has set CLICOLOR to “0” (or “1”) to enable color output, they’re doing it right, because that’s how CLICOLOR works — just as setting NO_COLOR to “0” (or “1”) disables color output, because that’s how NO_COLOR works.

  • Join the conversation

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