1

I am trying to write a coloured output library for bash with various styling options allowing colouring and styling using redirection.

e.g.

echo "Red" | red outputs red text

and

echo "Bold" | bold outputs bold text

and

echo "Yellow bold" | yellow | bold outputs bold yellow text

The code I wrote so far is as follows:

#shellcheck shell=bash

# set debug
# set -o xtrace

# number of colors supported
__colors=$(tput colors 2> /dev/null)
# colors
__black="$(tput setaf 0)"
__red="$(tput setaf 1)"
__green="$(tput setaf 2)"
__yellow="$(tput setaf 3)"
__blue="$(tput setaf 4)"
__magenta="$(tput setaf 5)"
__cyan="$(tput setaf 6)"
__white="$(tput setaf 7)"
# style
__default="$(tput sgr0)"
__bold="$(tput bold)"
__underline="$(tput smul)"


function has_colors() {
  COLOR=${COLOR:-auto}
  if [[ $COLOR = 'never' ]]; then
    return 1
  elif [[ $COLOR = 'always' ]]; then
    return 0
  else
    # check if stoud is terminal and terminal supports colors
    [[ -t 1 ]] && \
    [[ -n $__colors ]] && \
    [[ $__colors -ge 8 ]]
  fi
}

function __style() {
  read -r input
  if has_colors; then
    echo -e "$1" "$input" "$__default"
  else
    echo -e "$input"
  fi
}

function black() {
  __style "$__black"
}

function red() {
  __style "$__red"
}

function green() {
  __style "$__green"
}

function yellow() {
  __style "$__yellow"
}

function blue() {
  __style "$__blue"
}

function magenta() {
  __style "$__magenta"
}

function cyan() {
  __style "$__cyan"
}

function white() {
  __style "$__white"
}

function bold() {
  __style "$__bold"
}

function underline() {
  __style "$__underline"
}

Setting COLOR=always outputs with escape codes all the time. On the other hand COLOR=auto preforms some checks to make sure current stdout is the terminal and terminal supports colors.

The problem is using multiple styling options does not seem to be working.It always applies the last styling option. For example:

echo "Yellow bold" | yellow | bold outputs bold text, but not yellow.

On the other hand:

echo "Bold yellow" | bold | yellow outputs yellow text, but not bold.

Funny thing is; setting COLOR=always seems to be working just fine. So it looks like the test I perform to see if stdout is terminal [[ -t 1 ]] is causing this. I am not sure if it is because there is some kind of delay with that test. But when I remove [[ -t 1 ]] bit, it works.

Any idea how I can achieve this ? Not expert with Bash or how shell works for that matter. Quite confused here.

Bren
  • 2,148
  • 1
  • 27
  • 45
  • 1
    The problem is that in `yellow | bold`, the stdout of `yellow` is not a tty. Instead of checking in each color function, you should check once in the main script and set a variable that all functions use – that other guy Apr 15 '17 at 06:58
  • @that other guy, that is exactly why i check it. If I checked once and redirected output to a file it would be with color escape codes. Which i don't want – Bren Apr 15 '17 at 09:43
  • Looking at set -xv output, your issue seems to come from the fact that the bold (assuming it is second) is called prior to the echo -e line within style and receives a copy of the original string, hence you could do as many changes as you like inbetween and the result will always be standard text bolded. Somehow you need to have each pipe receive the data in turn (not sure on best method). You could look into the wait command?? – grail Apr 15 '17 at 10:27
  • You are making a two-way distinction (terminal vs not-terminal) when you really need some sort of *three*-way distinction (terminal vs file vs pipe). – chepner Apr 15 '17 at 12:30

1 Answers1

0

Looking at this with a clear head, I found the problem in my approach.

[[ -t 1 ]] tests if stdout is terminal, when I pipe two function like echo "Hello" | yellow | bold , [[ -t 1 ]] is false when going through function yellow denoting output is not a terminal.

Which is because output of the function( yellow) is piped into the second function (bold). That's why it does not output escape code for yellow and just outputs the input.

So If I keep piping to another function like echo "Hello" | yellow | bold | underline it will only underline the output.

This seemed to be a nice and easy way of outputting with colour, but now I have to change my approach unless there is a way to know if currently running function is being piped but not redirected?

EDIT

This post shows there is a way to detect if command is being redirected or being piped.

Still in the end this approach doesn't seem quite feasible because if I do not disable colour when piped it will output colour codes when piped to another command that is not another output styling function; That maybe an edge case, but still solution is not %100 fault proof

EDIT Solution:

Changed the approach. Instead of piping formats one after another using next format options as parameters to first one like below does the trick

echo "Hello" | yellow bold underline

The final code is follows:

#shellcheck shell=bash

# set debug
# set -xv

# number of colors supported
__colors=$(tput colors 2> /dev/null)

# foreground colors
__black="$(tput setaf 0)"
__red="$(tput setaf 1)"
__green="$(tput setaf 2)"
__yellow="$(tput setaf 3)"
__blue="$(tput setaf 4)"
__magenta="$(tput setaf 5)"
__cyan="$(tput setaf 6)"
__white="$(tput setaf 7)"

# background colors
__bg_black="$(tput setab 0)"
__bg_red="$(tput setab 1)"
__bg_green="$(tput setab 2)"
__bg_yellow="$(tput setab 3)"
__bg_blue="$(tput setab 4)"
__bg_magenta="$(tput setab 5)"
__bg_cyan="$(tput setab 6)"
__bg_white="$(tput setab 7)"

# style
__reset="$(tput sgr0)"
__bold="$(tput bold)"
__underline="$(tput smul)"

function has_colors() {
  COLOR=${COLOR:-auto}
  if [[ $COLOR = 'never' ]]; then
    return 1
  elif [[ $COLOR = 'always' ]]; then
    return 0
  else
    [[ -t 1 ]] && [[ -n $__colors ]] && [[ $__colors -ge 8 ]]
  fi
}

function __format() {
  local format="$1"
  local next="${2:-}" # next formatting function e.g. underline
  if has_colors; then
    echo -en "$format"
    if [[ -n $next ]]; then
      shift 2
      tee | "$next" "$@"
    else
      tee
      echo -en "$__reset"
    fi
  else
    tee #print output
  fi
}

function black() { __format "$__black" "$@"; }
function red() { __format "$__red" "$@"; }
function green() { __format "$__green" "$@";}
function yellow() { __format "$__yellow" "$@"; }
function blue() { __format "$__blue" "$@"; }
function magenta() { __format "$__magenta" "$@";}
function cyan() { __format "$__cyan"  "$@";}
function white() { __format "$__white"  "$@";}

function bg_black() { __format "$__bg_black" "$@"; }
function bg_red() { __format "$__bg_red" "$@"; }
function bg_green() { __format "$__bg_green" "$@";}
function bg_yellow() { __format "$__bg_yellow" "$@"; }
function bg_blue() { __format "$__bg_blue" "$@"; }
function bg_magenta() { __format "$__bg_magenta" "$@";}
function bg_cyan() { __format "$__bg_cyan"  "$@";}
function bg_white() { __format "$__bg_white"  "$@";}

function bold() { __format "$__bold"  "$@";}
function underline() { __format "$__underline" "$@"; }
Community
  • 1
  • 1
Bren
  • 2,148
  • 1
  • 27
  • 45
  • 1
    The best way to tell if a pipeline is being sent to a pipe or is being redirected is to inspect the command. Check in the main shell on startup if stdout is a tty (with `test -t`) and set a global variable. In each function, examine that variable. Within the script, if you redirect a pipeline to a file don't invoke your colorization functions! Personal opinion: don't bother with any of this. Colorization looks great, but the shell is too fragile for this sort of thing and eventually you will be looking at a log file at 3:00 am without enough coffee and wish it wasn't cluttered with cruft! – William Pursell Apr 15 '17 at 11:41
  • @WilliamPursell I am writing to be a reusable library and I think I will sometime in the future will redirect stuff to a file. I think if I crack this once it is going to be very very helpful for me. It is really only for personal use for now. If it proves to be useful and with proper testing than I think it will save a lot hassle. Or not ? I updated my answer and I think I found a good simple solution to this. I know ssh is different story, will have a play to see how it works through ssh or sockets. Any thoughts ? – Bren Apr 15 '17 at 12:25
  • 1
    Your solution is nice (but it does generate extra io and fails for multi line output; probably cleaner to just do `red; underline; echo some text; default`), but inevitably this sort of thing becomes more noise than it's worth. Simplicity in the code eventually outweighs the benefit of the colorized output. And "eventually" happens surprisingly sooner than expected! – William Pursell Apr 15 '17 at 12:30
  • @WilliamPursell Yup, I hear you :) – Bren Apr 15 '17 at 12:33