Version 3 of Colors in Unix Shells

Updated 2012-11-09 14:32:34 by RLE

Using colors when printing text to a console can be quite helpful for a program's users and, under Unix-like operating systems, it is natively supported by almost any shell.

The following code shows the ccolor namespace containing several variables with common color codes and a procedure for parsing text containing tags.

On the bottom of the page you can find examples on how to use it.


# ########################################################################### #
# Description:
#  This namespace contains console color codes defined in variables which 
#  can be used for adding color in text strings.
#  Additionally, it contains a procedure which replaces certain tags in a 
#  text with their corresponding color codes, making the resulting string 
#  suitable for dumping to a console supporting colors.
#  |-------------------|--------------------|------------------|
#  |Effects            |Foreground Colors   |Background Colors |
#  |-------------------|--------------------|------------------|
#  |bold       : \e[1m |black      : \e[30m |black   : \e[40m  |
#  |-------------------|--------------------|------------------|
#  |dim        : \e[2m |red        : \e[31m |red     : \e[41m  |
#  |-------------------|--------------------|------------------|
#  |underlined : \e[4m |green      : \e[32m |green   : \e[42m  |
#  |-------------------|--------------------|------------------|
#  |blink      : \e[5m |yellow     : \e[33m |yellow  : \e[43m  |
#  |-------------------|--------------------|------------------|
#  |reverse    : \e[7m |blue       : \e[34m |blue    : \e[44m  |
#  |-------------------|--------------------|------------------|
#  |invisible  : \e[8m |magenta    : \e[35m |magenta : \e[45m  |
#  |-------------------|--------------------|------------------|
#  |                   |cyan       : \e[36m |cyan    : \e[46m  |
#  |-------------------|--------------------|------------------|
#  |                   |white      : \e[37m |white   : \e[47m  |
#  |-------------------|--------------------|------------------|
#  |                   |default    : \e[39m |default : \e[49m  |
#  |-------------------|--------------------|------------------|
#
#  To reset the color codes:
#  - reset      : \e[0
#  Helpful control characters:
#  - backspace : \b
#
# Examples:
#  puts [ccolor::replace "<b>Blue on a <by>yellow background</>."]
#  puts [ccolor::replace "<u><gb>Underline</> and <v><r>reverse</>."]
# ########################################################################### #
namespace eval ccolor {
  variable reset   [binary format a4 \x1b\x5b\x30\x6d]
  # Helpful control characters #
  variable backspace [binary format a1 \x08]
  # Foreground #
  variable bold       [binary format a4 \x1b\x5b\x31\x6d]
  variable dim        [binary format a4 \x1b\x5b\x32\x6d]
  variable underlined [binary format a4 \x1b\x5b\x34\x6d]
  variable blink      [binary format a4 \x1b\x5b\x35\x6d]
  variable reverse    [binary format a4 \x1b\x5b\x37\x6d]
  variable invisible  [binary format a4 \x1b\x5b\x39\x6d]
  # Foreground colors #
  variable black   [binary format a5 \x1b\x5b\x33\x30\x6d]
  variable red     [binary format a5 \x1b\x5b\x33\x31\x6d]
  variable green   [binary format a5 \x1b\x5b\x33\x32\x6d]
  variable yellow  [binary format a5 \x1b\x5b\x33\x33\x6d]
  variable blue    [binary format a5 \x1b\x5b\x33\x34\x6d]
  variable magenta [binary format a5 \x1b\x5b\x33\x35\x6d]
  variable cyan    [binary format a5 \x1b\x5b\x33\x36\x6d]
  variable white   [binary format a5 \x1b\x5b\x33\x37\x6d]
  variable def     [binary format a5 \x1b\x5b\x33\x39\x6d]
  # Background colors #
  variable bblack   [binary format a5 \x1b\x5b\x34\x30\x6d]
  variable bred     [binary format a5 \x1b\x5b\x34\x31\x6d]
  variable bgreen   [binary format a5 \x1b\x5b\x34\x32\x6d]
  variable byellow  [binary format a5 \x1b\x5b\x34\x33\x6d]
  variable bblue    [binary format a5 \x1b\x5b\x34\x34\x6d]
  variable bmagenta [binary format a5 \x1b\x5b\x34\x35\x6d]
  variable bcyan    [binary format a5 \x1b\x5b\x34\x36\x6d]
  variable bwhite   [binary format a5 \x1b\x5b\x34\x37\x6d]
  variable bdef     [binary format a5 \x1b\x5b\x34\x39\x6d]

  # ########################################################################### #
  # Description:
  #  This procedure will parse the given input text and will replace all tags 
  #  with the corresponding color code. The tags are:
  #    |---------------------|------------------|--------------------------|
  #    |Effects              |Foreground Colors |Background Colors         |
  #    |---------------------|------------------|--------------------------|
  #    |<e> : bold (emphasis)|<k> : black       |<kb> : black background   |
  #    |---------------------|------------------|--------------------------|
  #    |<u> : underlined     |<r> : red         |<rb> : red background     |
  #    |---------------------|------------------|--------------------------|
  #    |<d> : dim            |<g> : green       |<gb> : green background   |
  #    |---------------------|------------------|--------------------------|
  #    |<f> : blink (flash)  |<y> : yellow      |<yb> : yellow background  |
  #    |---------------------|------------------|--------------------------|
  #    |<v> : reVerse        |<b> : blue        |<bb> : blue background    |
  #    |---------------------|------------------|--------------------------|
  #    |<i> : invisible      |<m> : magenta     |<mb> : magenta background |
  #    |---------------------|------------------|--------------------------|
  #    |                     |<c> : cyan        |<cb> : cyan background    |
  #    |---------------------|------------------|--------------------------|
  #    |                     |<w> : white       |<wb> : white background   |
  #    |---------------------|------------------|--------------------------|
  #    |                     |<d> : default     |<db> : default background |
  #    |---------------------|------------------|--------------------------|
  #
  #  To reset text printing to its normal behavior, use:
  #   </> : reset
  #  If you want to keep one of these tags unchanged in your text, please 
  #  escape them by pre-pending an additional '<' character. For example:
  #   "The <<r> tag sets the text color to <r>red</>"
  #   "The <</> tag resets printed text to its normal behavior."
  #  Additional helpful control tag:
  #   <bs> : backspace
  #  Examples of legal tagged text:
  #   "This text is <r>red</> and this one is <e><g>bold green</>."
  #   "<b>Blue on a <yb>yellow background</>."
  # ########################################################################### #
  proc replace {taggedText} {
    # Make sure the escaped tags do not get replaced #
    regsub -all -- "<<" $taggedText "<< " taggedText
    # Effects #
    regsub -all -- "<e>" $taggedText $ccolor::bold       taggedText
    regsub -all -- "<d>" $taggedText $ccolor::dim        taggedText
    regsub -all -- "<u>" $taggedText $ccolor::underlined taggedText
    regsub -all -- "<f>" $taggedText $ccolor::blink      taggedText
    regsub -all -- "<v>" $taggedText $ccolor::reverse    taggedText
    regsub -all -- "<i>" $taggedText $ccolor::invisible  taggedText
    # Foreground colors #
    regsub -all -- "<k>" $taggedText $ccolor::black      taggedText
    regsub -all -- "<r>" $taggedText $ccolor::red        taggedText
    regsub -all -- "<g>" $taggedText $ccolor::green      taggedText
    regsub -all -- "<y>" $taggedText $ccolor::yellow     taggedText
    regsub -all -- "<b>" $taggedText $ccolor::blue       taggedText
    regsub -all -- "<m>" $taggedText $ccolor::magenta    taggedText
    regsub -all -- "<c>" $taggedText $ccolor::cyan       taggedText
    regsub -all -- "<w>" $taggedText $ccolor::white      taggedText
    regsub -all -- "<d>" $taggedText $ccolor::def        taggedText
    # Background colors #
    regsub -all -- "<kb>" $taggedText $ccolor::bblack   taggedText
    regsub -all -- "<rb>" $taggedText $ccolor::bred     taggedText
    regsub -all -- "<gb>" $taggedText $ccolor::bgreen   taggedText
    regsub -all -- "<yb>" $taggedText $ccolor::byellow  taggedText
    regsub -all -- "<bb>" $taggedText $ccolor::bblue    taggedText
    regsub -all -- "<mb>" $taggedText $ccolor::bmagenta taggedText
    regsub -all -- "<cb>" $taggedText $ccolor::bcyan    taggedText
    regsub -all -- "<wb>" $taggedText $ccolor::bwhite   taggedText
    regsub -all -- "<db>" $taggedText $ccolor::bdef     taggedText
    # Reset #
    regsub -all -- "</>"  $taggedText $ccolor::reset taggedText
    # Other control characters #
    regsub -all -- "<bs>"  $taggedText $ccolor::backspace taggedText
    # Re-establish the escaped tags #
    regsub -all -- "<< " $taggedText "<<" taggedText
    # Un-escape them #
    regsub -all -- "<<e>"  $taggedText "<e>"  taggedText
    regsub -all -- "<<d>"  $taggedText "<d>"  taggedText
    regsub -all -- "<<u>"  $taggedText "<u>"  taggedText
    regsub -all -- "<<f>"  $taggedText "<f>"  taggedText
    regsub -all -- "<<v>"  $taggedText "<v>"  taggedText
    regsub -all -- "<<i>"  $taggedText "<i>"  taggedText
    regsub -all -- "<<k>"  $taggedText "<k>"  taggedText
    regsub -all -- "<<r>"  $taggedText "<r>"  taggedText
    regsub -all -- "<<g>"  $taggedText "<g>"  taggedText
    regsub -all -- "<<y>"  $taggedText "<y>"  taggedText
    regsub -all -- "<<b>"  $taggedText "<b>"  taggedText
    regsub -all -- "<<m>"  $taggedText "<m>"  taggedText
    regsub -all -- "<<c>"  $taggedText "<c>"  taggedText
    regsub -all -- "<<w>"  $taggedText "<w>"  taggedText
    regsub -all -- "<<d>"  $taggedText "<d>"  taggedText
    regsub -all -- "<<kb>" $taggedText "<kb>" taggedText
    regsub -all -- "<<rb>" $taggedText "<rb>" taggedText
    regsub -all -- "<<gb>" $taggedText "<gb>" taggedText
    regsub -all -- "<<yb>" $taggedText "<yb>" taggedText
    regsub -all -- "<<bb>" $taggedText "<bb>" taggedText
    regsub -all -- "<<mb>" $taggedText "<mb>" taggedText
    regsub -all -- "<<cb>" $taggedText "<cb>" taggedText
    regsub -all -- "<<wb>" $taggedText "<wb>" taggedText
    regsub -all -- "<<db>" $taggedText "<db>" taggedText
    regsub -all -- "<</>"  $taggedText "</>"  taggedText
    regsub -all -- "<<bs>" $taggedText "<bs>" taggedText
    # Return the changed text #
    return $taggedText
  }
}

Here's a short example on how to use it (assuming the code has been saved in a file named ccolor.tcl):

tclsh8.5 [~]source ccolor.tcl
tclsh8.5 [~]append tagged_text "The following is a list of categories (<e>not automatically updated, so there could be some missing</>) with short descriptions:\n"
tclsh8.5 [~]append tagged_text " * <b><u>Category Category</> - the meta category - covers the list of all categories.\n"
tclsh8.5 [~]append tagged_text " * <b><u>Category Uncategorized</> - the \"<d>anti-category</>\" - put on a page as a reminder that it hasn't really been categorized yet.\n"
tclsh8.5 [~]append tagged_text " * <b><u>Category 3D Graphics</> - pages relating to <g><v>3D graphical</> display of information\n"
tclsh8.5 [~]append tagged_text " * <b><u>Category Broken Links</> - used in connection with the <i>Broken Link Report\n"
tclsh8.5 [~]puts [ccolor::replace $tagged_text]

Or, if you want to use the namespace variables directly in a text string:

tclsh8.5 [~]puts " * ${ccolor::blue}Category AI${ccolor::reset} - pages relating to Artificial Intelligence"
tclsh8.5 [~]puts " * ${ccolor::blue}Category Critcl${ccolor::reset} - discussion of the Tcl runtime compile extension ${ccolor::bgreen}critcl${ccolor::reset}"

RLE (2012-11-09): Is there a reason you chose to use a slew of sequential regsub calls in your replace proc instead of building up a string map list and using string map to perform the replacements of the <?> tags with the appropriate color escape values?

I.e. (this is only for four colors, I didn't want to copy and edit everything here):

  set replacements [ list <<e> <e>    <<d> <d>    <<u> <u>    <<f> <f>
                          <e> $ccolor::bold       <d> $ccolor::dim
                          <u> $ccolor::underlined <f> $ccolor::blink ]

  set replaced_text [ string map $replacements $taggedText ]

Note that the first replacements in the list (<<e> <e> ...) must remain ahead of the color replacements, because they handle your escaping mechanism.

One call to string map, with one pass over the string will be significantly faster than 54 individual regsubs which each have to traverse the string in full each time to make each single replacement.

The replacements list could be built up with a foreach loop to save typing as well (this assumes at least Tcl 8.5):

  foreach {echar colorvar} {e bold    d dim   u underlined   f blink} {
    lappend replacements <<$echar> <$echar>
    lappend list2 <$echar> [ set ccolor::$colorvar ]
  }
  lappend replacements {*}$list2

If you run the foreach in the namespace initialization code, and store the results in a namespace variable, then the foreach only needs to execute once, when the namespace is loaded.

Also, the way you are accessing the namespace "ccolor" variables has a dependency upon the implicit manner in which Tcl resolves namespace paths. As long as your namespace is only ever loaded into the root namespace (::) everything will work. But if your namespace is ever loaded inside another namespace (so the path becomes anotherNamespace::ccolor::bold) the variable look-up will fail.