Jack
Jack

Reputation: 3

How do you make a shell function that you can pipe into to make the output red?

I want something where I can do {some command} | red and it'll output whatever {some command} would have outputted, but red. The best I've been able to do is

function red()
{
    while read text
    do
        echo -e '\033[31m'$text'\033[0m'
    done
}

But this removes all the indentation.

This seems like something that should be easy but I just can't seem to figure it out. I was trying to do it just for fun but now I just need to know, 'cause there has to be a simple way to do this that I'm missing.

EDIT:

I've also tried this in C like so:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char* buffer = malloc(8);
    int size     = 8;
    int index    = 0;

    while ((buffer[index++] = getchar()) != EOF)
    {
        if (index == size)
        {
            size *= 2;
            buffer = realloc(buffer, size);
        }
    }
    buffer[index] = 0;

    printf("%s%s%s", "\\033[31m", buffer, "\\033[0m");
}

But the shell doesn't interpret the escape characters, and so doesn't make the output red.

Upvotes: 0

Views: 102

Answers (2)

user1934428
user1934428

Reputation: 22227

Writing the escape codes explicitly is IMO cumbersome. I would do something like

red() {
  # Send stdin to stdout, but coloured in red, if the
  # terminal supports it.
  if tput setaf 196 2>/dev/null
  then
    cat
    tput sgr0 # reset the attribute
  else
    # No support for changing the colour. Just display it
    # using the default foreground colour
    cat
  fi
}

which also works on terminals which are not configured for color.

The magical number 196 comes from this table.

Upvotes: 1

L&#233;a Gris
L&#233;a Gris

Reputation: 19575

It was only missing a couple things for it to work:

  • Firstly you want to neutralize the Internal Field Separator IFS environment variable, because it was the cause of your discarded indentation. This variable caused the read operation to eat leading tabs and spaces as field separator.
  • Next you wan to read -r. The -r flag prevent the interpretation of escaped characters and have a raw read instead.
  • Finally, you must not use echo -e. It is a horrible non-standard way of printing \ escaped characters. Use a printf instead.
  • Additionally, the loop on the read will not handle the last line of text if it does not contain an newline character \n. The shell read returns false in this case after reading the remaining text, and break out of the while loop without processing the last line not ending with a newline.
    It is dealt with testing if the line contains a last line of text and print it without a newline for this special last case.

Now here it is all fixed and working as you intended to:

#!/usr/bin/env bash

function red() {
  while IFS= read -r line; do
    printf '\e[31m%s\e[0m\n' "$line"
  done
  [ -n "$line" ] && printf '\e[31m%s\e[0m' "$line"
}

red

EDIT:

As @Thsutton commented above, you can use cat in the red function to insert the ANSI CSI sequences before and after the whole text only, rather than for each line of text. It does not need a while loop, so will undoubtedly be more performant. Although note that the per-line method might give better results if the text stream itself contains ANSI CSI sequences changing the terminal colour.

red() {
  printf '\e[31m'
  cat
  printf '\e[0m'
}

Upvotes: 1

Related Questions