I like to user colored output in my interactive shell scripts.

I've been using Ubuntu and bash for a long time, but haven't done much bash scripting.

I'd appreciate a review of this function for generating colored output.

#!/usr/bin/env bash
# Function to generate colored output.
# The output is specified using a format string and arguments, similar to printf.
# All special format specifiers are enclosed in parenthesis.
# Colors start with either "f" for foreground or "b" for background with the
# remainder of the specifier being the color name, starting with a capital
# letter (e.g., fRed or bCyan).
# There are a few other specifiers in addition to the colors:
# - %s%         Placeholder; consumes the next argument from the command line
# - %u%         Underline
# - %r$         Reverse video
# - %default%   Default foreground and background color
# - %reset%     Reset all attributes
# Example:
#   echoc '%fRed%The value "%r%%s%%fRed%" is not valid for the --trace option' extra
# Result:
#   The value "extra" is not valid for the --trace option
#   with everything except the word extra being red on the default background
#   color and the word extra being reverse-video.
unset -f echoc
function echoc()
  local       char
  local       code
  local       fmt="${1}"
  local       fmtLen=${#fmt}
  local       i=0
  local       output=''
  local       str=''
  local       tagActive=0

  shift       # Remove the format string.

  # The ANSI color and format codes.
  # Keys that start with "f" are foreground colors.
  # Keys that start with "b" are background colors.
  # Other keys are eight resets or other formatting codes.
  # The "r" and "u" are reverse and underscore, respectively.
  declare -A colors
  colors=(['fBlack']='30' ['fBlue']='34' ['fBrown']='33' ['fCyan']='36')
  colors+=(['fDarkgray']='1;30' ['fDefault']='39' ['fGreen']='32' ['fLavender']='1;34')
  colors+=(['fLightblue']='1;36' ['fLightgray']='37' ['fLightgreen']='1;32')
  colors+=(['fLightpurple']='1;35' ['fMagenta']='35' ['fPink']='1;31' ['fPurple']='35')
  colors+=(['fRed']='31' ['fWhite']='1;37' ['fYellow']='1;33')
  colors+=(['bBlack']='40' ['bBlue']='44' ['bBrown']='43' ['bCyan']='46' ['bDefault']='49')
  colors+=(['bGreen']='42' ['bMagenta']='45' ['bPurple']='45' ['bRed']='41')
  colors+=(['default']='39;49' ['reset']='0')
  colors+=(['r']='7' ['u']='4')

  # Parse the format string, building the output string as we go.
  while [ $i -lt $fmtLen ]
    case $char in
        if [ 0 -eq $tagActive ]
          if [ -n "${str}" ]
        else # tagActive is true.
          if [ -z "${str}" ]
            output="${output}%" # %% inserts a percent sign.
            if [ 's' == "${str}" ]
              if [ -z "${code}" ]
                echo -e "\033[31mInvalid color name \"${str}\"\033[0m"

  if [ -n "${str}" ]
  echo -e "${output}"

export -f echoc

Three things strike me immediately:

  1. You are treating foreground and background colors equally, and giving preference to reverse video and underline(?). Why is red foreground %fRed% while reverse video is %r%? Aren't you more likely to use red,yellow,green foreground than reverse video? Aren't you more likely to use foreground colors than background colors?

    I think you should give some thought to the "Huffmanization" of your color names. Maybe you need %r% %g% %y% as shortcuts for foreground red, green, and yellow. (Or, maybe not. It depends on what your use case is going to be. If you're writing a rogue-like in bash, maybe you have a different color vocabulary...)

    I would suggest that foreground colors just be color names, while background colors have a bg prefix: %cyan% versus %bgCyan% for example. This makes background colors more expensive than they currently are (1 extra character) but makes foreground colors less expensive, and I suspect they'll be more common.

  2. Next, I think you're doing too much work inside your loop. You are actually processing the format string character-by-character! Given that the beginning delimiter and ending delimiter are the same - % - you could use IFS to split the string into an array and process the segments of the array. Given a string like "before % during % after", or even "%during%", you know that the alternate members of the array are going to be tags: even indices text, odd indices tag.

    See this answer on SO for how to split a string.

    This would make change your loop from processing characters into processing "strings", and should speed up your function.

  3. Finally, there are a lot of "magic strings" in your code. Like magic numbers, you should replace these with constants (variables) and reference them just once. In particular, I'm thinking of ${pfx} ("\033[") and maybe ${reset} ("${pfx}0m").

  • \$\begingroup\$ thanks for the input. I don't agree that I'm givin g preferential treatment to anything. I chose these names for clarity in other scripts. Using IFS to split the string is an interesting idea. I'll look into it. There is nothing "magic" about the escape sequences. Although, I am going to look into tput as recommended by Tony Speight. \$\endgroup\$ – David Patterson Feb 5 '18 at 23:53

The escape sequences beginning with the ESC sequence (\033) are particular to the kind of terminal or terminal-emulator you're using. You could make the code much more portable to terminals with different control codes or without colour/bold support if you instead use the tput program to generate the correct output to match the $TERM that's in use.

  • \$\begingroup\$ thanks for the feedback. I didn't know about tput and will play around with it (although I've seen mention in preliminary searching that it's broken on some distributions). \$\endgroup\$ – David Patterson Feb 5 '18 at 23:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.