Bash function to generate colored output 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 ]
  do
    char="${fmt:i:1}"
    case $char in
      %)
        if [ 0 -eq $tagActive ]
        then
          if [ -n "${str}" ]
          then
            output="${output}${str}"
            str=''
          fi
          tagActive=1
        else # tagActive is true.
          if [ -z "${str}" ]
          then
            output="${output}%" # %% inserts a percent sign.
          else
            if [ 's' == "${str}" ]
            then
              output="${output}${1}\033[0m"
              shift
            else
              code="${colors[$str]}"
              if [ -z "${code}" ]
              then
                echo -e "\033[31mInvalid color name \"${str}\"\033[0m"
              else
                output="${output}\033[${code}m"
              fi
            fi
            str=''
          fi
          tagActive=0
        fi
        ;;

      *)
        str="${str}${char}"
        ;;
    esac
    ((i++))
  done
  if [ -n "${str}" ]
  then
    output="${output}${str}"
  fi
  output="${output}\033[0m"
  echo -e "${output}"

}
export -f echoc
Three things strike me immediately:


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.
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.
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").