# 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  ## 2 Answers 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").

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

• 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). – David Patterson Feb 5 '18 at 23:54