Friendly Output: Columns and Colors

printf lines output up in columns, with a width for every blank it fills, and tput adds bold and color, but only when [[ -t 1 ]] says the output is going to a terminal, so files and pipes get plain text.

Journeyman · Unix · Linux · Updated

After this lesson you can

  • Line up output in columns with printf, using %-22s for left-aligned and %7s for right-aligned fields
  • Color a script's output with tput, only when [[ -t 1 ]] says the output is going to a terminal

Key takeaways

  • printf fills the blanks in a pattern: %s is a blank for text, and \n ends the line. printf never adds a new line of its own.
  • %-22s fills a blank 22 characters wide, lined up on the left; %7s lines up on the right, which suits numbers. A longer value isn't cut, so pick widths that fit.
  • tput bold, tput setaf 2 (green) and tput sgr0 (back to normal) print the codes a terminal reads as styles and colors.
  • Color codes in a file or a pipe are junk. [[ -t 1 ]] is true only when the output goes to a terminal, so set the colors only then.

Ten recipes in sentences, such as Taco Tuesday (25 min, dinner), are fine. Forty make your eyes hunt for the minutes in every line. Compare ls -l or df -h: a header and neat columns, so you read straight down. Many tools add a little color too: ls paints folders blue and grep highlights what it found. Columns and color are what make a tool pleasant to use every day. Color needs one bit of care, though, and this lesson covers that as well.

printf: a line with blanks to fill

A form letter has blanks, "Dear ___", filled in for each person. printf (read "print f", print formatted) works the same way: a pattern with blanks, then the values to fill them, in order. %s (read "a string goes here") is a blank for any text:

printf '%s takes %s minutes\n' 'Taco Tuesday' 25
Taco Tuesday takes 25 minutes

Unlike echo, printf never starts a new line by itself, so the pattern ends with \n (read "new line").

Columns: a width for each blank

Put a number in a blank to give it a width. %-15s (read "15 wide, on the left") pads the text with spaces on its right, and %5s lines it up on the right. The brackets show where the spaces go:

printf '[%-15s][%5s]\n' 'Tomato Soup' 20
printf '[%-15s][%5s]\n' 'Slow Cooker Chili' 45
[Tomato Soup    ][   20]
[Slow Cooker Chili][   45]

Numbers on the right line up their ones under their ones. A value longer than its width isn't cut off; it pushes the rest of the line over, so give each column room for its longest value.

In recipes, in cmd_list, add a header line right before the while read line, and change the echo inside the loop to a printf with the same widths:

  printf '%-22s %7s  %s\n' NAME MINUTES CATEGORY
    printf '%-22s %7s  %s\n' "$name" "$minutes" "$category"
./recipes list -c dinner
NAME                   MINUTES  CATEGORY
Taco Tuesday                25  dinner
Slow Cooker Chili           45  dinner

Colors with tput

In A Prompt in Your Colors, \e[1;36m turned on bold cyan. Those ANSI escape codes are hard to remember. tput (read "t put", terminal put) looks up the right code for your terminal by name: tput bold, tput setaf 2 ("set foreground color 2", green) and tput sgr0 ("back to normal"). cat -v shows the codes it prints, with ^[ for the escape character (the echo only ends the line):

tput bold | cat -v; echo
^[[1m

Add these lines under source "$config_file", and use them in two places: the header line becomes printf '%s%-22s %7s %s%s\n' "$bold" NAME MINUTES CATEGORY "$reset", and cmd_add says echo "${green}Added $3.${reset}".

bold=$(tput bold)
green=$(tput setaf 2)
reset=$(tput sgr0)

Now the header is bold and "Added" is green on your screen.

Ubuntu's terminal after ./recipes add 30 dinner "Lemon Garlic Pasta" and ./recipes list -c dinner: the line Added Lemon Garlic Pasta. in green, then the header NAME MINUTES CATEGORY in bold, and three dinners in plain columns below it The color numbers are the prompt lesson's, less 30: 1 is red, 2 green, 4 blue.

Color only on a terminal

Save the list into a file, the way you'd keep a copy, and look inside:

./recipes list > list.txt
cat -v list.txt | head -n 3
^[[1mNAME                   MINUTES  CATEGORY^[(B^[[m
Taco Tuesday                25  dinner
Overnight Oats               5  breakfast

The codes went into the file as junk, and they'd confuse a program reading the list through a pipe too. The fix is a question: is the output going to a terminal? [[ -t 1 ]] (read "is stream 1 a terminal?") is true only then. Stream 1 is standard output, from Three Streams: Output, Errors and Input. Replace the three tput lines with:

if [[ -t 1 && -z "$NO_COLOR" ]]; then
  bold=$(tput bold)
  green=$(tput setaf 2)
  reset=$(tput sgr0)
else
  bold="" green="" reset=""
fi

NO_COLOR is a setting some people make to turn color off in every tool that respects it, so yours does too. Make the version recipes 1.3.0, and try again:

./recipes list > list.txt
cat -v list.txt | head -n 3
./recipes list | grep Soup
NAME                   MINUTES  CATEGORY
Taco Tuesday                25  dinner
Overnight Oats               5  breakfast
Tomato Soup                 20  lunch

Color on the screen, plain text everywhere else: ls and grep behave exactly this way. tput and [[ -t 1 ]] work the same on a Mac.

Check yourself

2 questions to try on the lesson page.

So what

Columns you can read straight down and color only where a person sees it turn a script's output into something you're glad to look at every day, and still safe to save or pipe.

Key terms

Cheat sheets

Related