On Colorized Output

Published on December 16, 2011 by Jesse Storimer

Colorized output should be configurable.

This is a followup to my last post.

In that post I said:

When you are piping output to another program you should always send plain, unformatted text. Unix utilities expect to deal with plain text.

A redditor commented on that post to this effect: (paraphrasing is mine)

Modern pagers and other programs can handle colorized output just fine, so colorized output from your scripts should be configurable.

If you're redirecting to a file, or some other program that doesn't understand escaped color sequences, then colorized output can be turned off, otherwise it'll probably work just fine.

Good advice, and he's absolutely right.

I started from the same script used in the last post and removed the check for $stdout.tty? so that colorized output is always on. Now to experiment.

$ hilong Gemfile | less
$ hilong Gemfile | grep gem
$ hilong Gemfile | more
$ hilong Gemfile > out.put

Your results may vary depending on your system and your configuration, but for me the first two experiments worked just fine and the second two didn't.

So grep and less handled the color codes fine, but more didn't. Redirecting to a file obviously just preserved the raw text, which won't be interpreted by text editors and the like when it's opened later.

So there needs to be some way to turn colorized output on...or off...

Sensible Default?

When in doubt, follow suit. Tools like grep and less don't colorize output by default. But ack, for instance, does colorize by default. So what should hilong do?

Given that the 'highlighting' is basically the point of this tool I think that colorized output should be enabled by default. For most modern tools colorized output will not cause a problem, disabling colorized output will be a special case.

But there needs to be an option for that.

Parsing command-line options

Ruby's standard library has a class just for parsing command-line options. It's called optparse. Using the script from the last blog post, here's how to use optparse to accept a --no-color option.

maximum_line_length = 80
colorized_output = true

require 'optparse'
OptionParser.new do |options|
  options.on("--no-color", "Disable colorized output") do |color|
    colorized_output = color

# Keep reading lines of input as long as they're coming.
while input = ARGF.gets
  input.each_line do |line|
    # Construct a string that begins with the length of this line
    # and ends with the content. The trailing newline is #chop'ped 
    # off of the content so we can control where the newline occurs.
    # The string are joined with a tab character so that indentation
    # is preserved.
    output_line = [line.size, line.chop].join("\t")

    # If the line is long and our $stdout is not being piped then we'll
    # colorize this line.
    if colorized_output && line.size > maximum_line_length
      # Turn the output to red starting at the first character.
      output_line.insert(0, red)

Something of interest is that colorized_output is assigned to true at the beginning, and then is assigned the value of the parsed option if that was passed in. Nowhere is it specified that --no-color should set colorized_output to false, it's simply assigned the value passed by optparse.

optparse encourages best practices by recognizing certain conventions. eg. --[no-]thing for boolean options, --arg value for required options.

So optparse was able to figure that --no-color is a boolean and that it should be negative (false) considering it's prefixed with a --no. Rad.

Notice that the check for $stdout.tty? has been removed. It no longer matters. Output is colorized unless disabled with the command-line option.

Now hilong can be used with that option to disable colorized output.

$ hilong --no-color Gemfile
$ cat Gemfile Gemfile.lock | hilong --no-color | more

Perfect. And thanks to optparse there's also some nice documentation showing which options are supported.

$ hilong -h
Usage: hilong [options]
        --no-color                   Disable colorized output

Not bad, but it's easy to make that look better.

require 'optparse'
OptionParser.new do |options|
  # This banner is the first line of the help documentation.
  options.banner = "Usage: hilong [options] [files]\n" \
    "Show character count for each line of input and highlight long lines."

  # Separator just adds a new line with the specified text.
  options.separator ""
  options.separator "Specific options:"

  options.on("--no-color", "Disable colorized output") do |color|
    colorized_output = color

  # on_tail says that this option should appear at the bottom of
  # the options list.
  options.on_tail("-h", "--help", "You're looking at it!") do
    $stderr.puts options
    exit 1

Now the help docs look like this:

$ hilong -h
Usage: hilong [options] [files]
Show character count for each line of input and highlight long lines.

Specific options:
        --no-color                   Disable colorized output
    -h, --help                       You're looking at it!

Much better.

Moar options

I want to show one more case with optparse. Currently the script assumes that long lines are 80 chars or more. It's generally accepted that 80 chars is a long line but some some devs push that number to 120, or something else entirely. The maximum line length should be configurable.


  options.on("-m", "--max NUM", Integer, "Maximum line length") do |max|
    maximum_line_length = max

Again optparse is making a few assumptions about the options, but it does a pretty good job of it. It's set to accept an Integer along with the -m or --max option. The result is assigned to the previously existing local variable and is handled by the rest of the script.

Now it can be used like so:

$ cat Gemfile* | hilong --max 20
$ hilong --m 140 Gemfile
$ hilong --max=180 Gemfile

Try to pass something besides an Integer with the max option and optparse rightfully complains.

Full source of the hilong script is at https://gist.github.com/1465437.

The ruby-core documentation for OptionParser contains a useful example if you want to know more.