]> git.refcnt.org Git - colorize.git/commitdiff
Initial commit.
authorSteven Schubiger <stsc@refcnt.org>
Tue, 18 Dec 2012 22:07:43 +0000 (23:07 +0100)
committerSteven Schubiger <stsc@refcnt.org>
Tue, 18 Dec 2012 22:07:43 +0000 (23:07 +0100)
Makefile [new file with mode: 0644]
colorize.c [new file with mode: 0644]
test.pl [new file with mode: 0755]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..b332cda
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+.PHONY: clean
+
+.SUFFIXES:
+.SUFFIXES: .c .o
+
+SHELL=/bin/sh
+CC=gcc
+CFLAGS=-Wall -Wextra -Wformat -Wswitch-default -Wuninitialized -Wunused
+
+colorize:      colorize.o
+                       $(CC) -o $@ $<
+
+colorize.o:    colorize.c
+                       $(CC) $(CFLAGS) -c $< -DCFLAGS="$(CFLAGS)"
+
+check:
+                       perl ./test.pl
+
+clean:
+                       [ -e colorize.o ] && rm colorize.o; exit 0
diff --git a/colorize.c b/colorize.c
new file mode 100644 (file)
index 0000000..7e9edcb
--- /dev/null
@@ -0,0 +1,730 @@
+/*
+ * colorize - Read text from standard input stream or file and print
+ *            it colorized through use of ANSI escape sequences
+ *
+ * Copyright (c) 2011-2012 Steven Schubiger
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#define _POSIX_SOURCE
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <getopt.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+
+#define str(arg) #arg
+#define to_str(arg) str(arg)
+
+#define streq(s1, s2) (strcmp (s1, s2) == 0)
+
+#if !defined BUF_SIZE || BUF_SIZE <= 0
+# undef BUF_SIZE
+# define BUF_SIZE 4096 + 1
+#endif
+
+#define LF 0x01
+#define CR 0x02
+
+#define SKIP_LINE_ENDINGS(flags) (((flags) & CR) && ((flags) & LF) ? 2 : 1)
+
+#define STACK_VAR(ptr) do {                                   \
+    stack_var (&vars_list, &stacked_vars, stacked_vars, ptr); \
+} while (false);
+
+#define RELEASE_VAR(ptr) do {                             \
+    release_var (vars_list, stacked_vars, (void **)&ptr); \
+} while (false);
+
+#define ABORT_TRACE()                                                              \
+    fprintf (stderr, "aborting in source file %s, line %d\n", __FILE__, __LINE__); \
+    abort ();                                                                      \
+
+#define CHECK_COLORS_RANDOM(color1, color2)        \
+     streq (color_names[color1]->name, "random")   \
+ && (streq (color_names[color2]->name, "none")     \
+  || streq (color_names[color2]->name, "default")) \
+
+#define VERSION "0.47"
+
+typedef unsigned short bool;
+
+enum { false, true };
+
+struct color_name {
+    char *name;
+    char *orig;
+};
+
+static struct color_name *color_names[3] = { NULL, NULL, NULL };
+
+struct color {
+    const char *name;
+    const char *code;
+};
+
+static const struct color fg_colors[] = {
+    { "none",     NULL },
+    { "black",   "30m" },
+    { "red",     "31m" },
+    { "green",   "32m" },
+    { "yellow",  "33m" },
+    { "blue",    "34m" },
+    { "cyan",    "35m" },
+    { "magenta", "36m" },
+    { "white",   "37m" },
+    { "default", "39m" },
+};
+static const struct color bg_colors[] = {
+    { "none",     NULL },
+    { "black",   "40m" },
+    { "red",     "41m" },
+    { "green",   "42m" },
+    { "yellow",  "43m" },
+    { "blue",    "44m" },
+    { "cyan",    "45m" },
+    { "magenta", "46m" },
+    { "white",   "47m" },
+    { "default", "49m" },
+};
+
+enum fmts {
+    FMT_GENERIC,
+    FMT_COLOR,
+    FMT_RANDOM,
+    FMT_ERROR,
+    FMT_FILE
+};
+static const char *formats[] = {
+    "%s",                    /* generic */
+    "%s color '%s' %s",      /* color   */
+    "%s color '%s' %s '%s'", /* random  */
+    "less than %d bytes %s", /* error   */
+    "%s: %s",                /* file    */
+};
+
+enum { FOREGROUND, BACKGROUND };
+
+static const struct {
+    struct color const *entries;
+    unsigned int count;
+    const char *desc;
+} tables[] = {
+    { fg_colors, sizeof (fg_colors) / sizeof (struct color), "foreground" },
+    { bg_colors, sizeof (bg_colors) / sizeof (struct color), "background" },
+};
+
+enum stream_mode { SCAN_FIRST = 1, SCAN_ALWAYS };
+
+struct ending {
+    unsigned int flags;
+    const char newline[3];
+};
+
+static const struct ending endings[] = {
+    { CR & LF, "\r\n" },
+    { CR,      "\r"   },
+    { LF,      "\n"   },
+};
+
+static FILE *stream = NULL;
+
+static unsigned int stacked_vars = 0;
+static void **vars_list = NULL;
+
+static char *exclude = NULL;
+
+static const char *program_name;
+
+static void print_help (void);
+static void print_version (void);
+static void cleanup (void);
+static void free_color_names (struct color_name **);
+static void process_options (unsigned int, char **, bool *, const struct color **, const char **, FILE **);
+static void read_print_stream (bool, const struct color **, const char *, FILE *, enum stream_mode);
+static void find_color_entries (struct color_name **, const struct color **);
+static void find_color_entry (const char *const, unsigned int, const struct color **);
+static void print_line (const struct color **, bool, const char * const, unsigned int);
+static char *xstrdup (const char *);
+static void vfprintf_fail (const char *, ...);
+static void stack_var (void ***, unsigned int *, unsigned int, void *);
+static void release_var (void **, unsigned int, void **);
+
+extern char *optarg;
+extern int optind;
+
+int
+main (int argc, char **argv)
+{
+    unsigned int arg_cnt = 0;
+
+    bool invalid_opt = false;
+
+    int opt;
+    struct option long_opts[] = {
+        { "exclude-random", required_argument, NULL, 'e' },
+        { "help",           no_argument,       NULL, 'h' },
+        { "version",        no_argument,       NULL, 'v' },
+        {  0,               0,                 0,     0  },
+    };
+
+    bool bold = false;
+
+    const struct color *colors[2] = {
+        NULL, /* foreground */
+        NULL, /* background */
+    };
+
+    const char *file;
+
+    enum stream_mode mode = SCAN_FIRST;
+
+    program_name = argv[0];
+    atexit (cleanup);
+
+    setvbuf (stdout, NULL, _IOLBF, 0);
+
+    while ((opt = getopt_long (argc, argv, "hv", long_opts, NULL)) != -1)
+      {
+        switch (opt)
+          {
+            case 'e': {
+              char *p;
+              exclude = xstrdup (optarg);
+              STACK_VAR (exclude);
+              for (p = exclude; *p; p++)
+                *p = tolower (*p);
+              if (streq (exclude, "random"))
+                vfprintf_fail (formats[FMT_GENERIC], "--exclude-random switch must be provided a color");
+              break;
+            }
+            case 'h':
+              print_help ();
+              exit (EXIT_SUCCESS);
+            case 'v':
+              print_version ();
+              exit (EXIT_SUCCESS);
+            case '?':
+              invalid_opt = true;
+              break;
+            default: /* never reached */
+              ABORT_TRACE ();
+          }
+      }
+
+    arg_cnt = argc - optind;
+
+    if (arg_cnt == 0 || arg_cnt > 2 || invalid_opt)
+      {
+        print_help ();
+        exit (EXIT_FAILURE);
+      }
+
+    process_options (arg_cnt, &argv[optind], &bold, colors, &file, &stream);
+    read_print_stream (bold, colors, file, stream, mode);
+
+    RELEASE_VAR (exclude);
+
+    exit (EXIT_SUCCESS);
+}
+
+static void
+print_help (void)
+{
+    unsigned int i;
+
+    printf ("Usage: %s (foreground) OR (foreground)/(background) [-|file]\n\n", program_name);
+    printf ("\tColors (foreground) (background)\n");
+    for (i = 0; i < tables[FOREGROUND].count; i++)
+      {
+        const struct color *entry = &tables[FOREGROUND].entries[i];
+        const char *name = entry->name;
+        const char *code = entry->code;
+        if (code)
+          printf ("\t\t{\033[%s#\033[0m} [%c%c]%s%*s%s\n",
+                   code, toupper (*name), *name, name + 1, 10 - (int)strlen (name), " ", name);
+        else
+          printf ("\t\t{-} %s%*s%s\n", name, 13 - (int)strlen (name), " ", name);
+      }
+    printf ("\t\t{*} [Rr]%s%*s%s [--exclude-random=<foreground color>]\n", "andom", 10 - (int)strlen ("random"), " ", "random");
+
+    printf ("\n\tFirst character of color name in upper case denotes increased intensity,\n");
+    printf ("\twhereas for lower case colors will be of normal intensity.\n");
+
+    printf ("\n\tOptions\n");
+    printf ("\t\t-h, --help\n");
+    printf ("\t\t-v, --version\n\n");
+}
+
+static void
+print_version (void)
+{
+    const char *c_flags;
+    printf ("%s v%s (compiled at %s, %s)\n", "colorize", VERSION, __DATE__, __TIME__);
+#ifdef CFLAGS
+    c_flags = to_str (CFLAGS);
+#else
+    c_flags = "unknown";
+#endif
+    printf ("Compiler flags: %s\n", c_flags);
+    printf ("Buffer size: %d bytes\n", BUF_SIZE - 1);
+}
+
+static void
+cleanup (void)
+{
+    free_color_names (color_names);
+
+    if (stream && fileno (stream) != STDIN_FILENO)
+      fclose (stream);
+
+    if (vars_list)
+      {
+        unsigned int i;
+        for (i = 0; i < stacked_vars; i++)
+          if (vars_list[i])
+            {
+              free (vars_list[i]);
+              vars_list[i] = NULL;
+            }
+        free (vars_list);
+        vars_list = NULL;
+      }
+}
+
+static void
+free_color_names (struct color_name **color_names)
+{
+    unsigned int i;
+    for (i = 0; color_names[i]; i++)
+      {
+        free (color_names[i]->name);
+        color_names[i]->name = NULL;
+        free (color_names[i]->orig);
+        color_names[i]->orig = NULL;
+        free (color_names[i]);
+        color_names[i] = NULL;
+      }
+}
+
+static void
+process_options (unsigned int arg_cnt, char **option_strings, bool *bold, const struct color **colors, const char **file, FILE **stream)
+{
+    int ret;
+    unsigned int index;
+    char *color, *p, *str;
+    struct stat sb;
+
+    const char *color_string = arg_cnt >= 1 ? option_strings[0] : NULL;
+    const char *file_string  = arg_cnt == 2 ? option_strings[1] : NULL;
+
+    assert (color_string);
+
+    if (streq (color_string, "-"))
+      {
+        if (file_string)
+          vfprintf_fail (formats[FMT_GENERIC], "hyphen cannot be used as color string");
+        else
+          vfprintf_fail (formats[FMT_GENERIC], "hyphen must be preceeded by color string");
+      }
+
+    ret = stat (color_string, &sb);
+
+    /* Ensure that we don't fail if there's a file with one or more
+       color names in its path.  */
+    if (ret != -1)
+      {
+        bool have_file;
+        unsigned int c;
+        const char *color = color_string;
+
+        for (c = 1; c <= 2 && *color; c++)
+          {
+            bool matched = false;
+            unsigned int i;
+            for (i = 0; i < tables[FOREGROUND].count; i++)
+              {
+                const struct color *entry = &tables[FOREGROUND].entries[i];
+                char *p;
+                if ((p = strstr (color, entry->name)) && p == color)
+                  {
+                    color = p + strlen (entry->name);
+                    matched = true;
+                    break;
+                  }
+              }
+            if (matched && *color == '/' && *(color + 1))
+              color++;
+            else
+              break;
+          }
+
+        have_file = (*color != '\0');
+
+        if (have_file)
+          vfprintf_fail (formats[FMT_GENERIC], "file must be preceeded by color string");
+      }
+
+    if ((p = strchr (color_string, '/')))
+      {
+        if (p == color_string)
+          vfprintf_fail (formats[FMT_GENERIC], "foreground color missing");
+        else if (p == color_string + strlen (color_string) - 1)
+          vfprintf_fail (formats[FMT_GENERIC], "background color missing");
+        else if (strchr (++p, '/'))
+          vfprintf_fail (formats[FMT_GENERIC], "one color pair allowed only");
+      }
+
+    str = xstrdup (color_string);
+    STACK_VAR (str);
+
+    for (index = 0, color = str; *color; index++, color = p)
+      {
+        char *ch, *sep;
+        if ((sep = strchr (color, '/')))
+          {
+            *sep = '\0';
+            p = sep + 1;
+          }
+        else
+          p = color + strlen (color);
+
+        for (ch = color; *ch; ch++)
+          if (!isalpha (*ch))
+            vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color, "cannot be made of non-alphabetic characters");
+
+        for (ch = color + 1; *ch; ch++)
+          if (!islower (*ch))
+            vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color, "cannot be in mixed lower/upper case");
+
+        if (streq (color, "None"))
+          vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color, "cannot be bold");
+
+        if (isupper (*color))
+          {
+            switch (index)
+              {
+                case FOREGROUND:
+                  *bold = true;
+                  break;
+                case BACKGROUND:
+                  vfprintf_fail (formats[FMT_COLOR], tables[BACKGROUND].desc, color, "cannot be bold");
+                  break;
+                default: /* never reached */
+                  ABORT_TRACE ();
+              }
+          }
+
+        color_names[index] = malloc (sizeof (struct color_name));
+
+        color_names[index]->orig = xstrdup (color);
+
+        for (ch = color; *ch; ch++)
+          *ch = tolower (*ch);
+
+        color_names[index]->name = xstrdup (color);
+      }
+
+    RELEASE_VAR (str);
+
+    assert (color_names[FOREGROUND]);
+
+    if (color_names[BACKGROUND])
+      {
+        unsigned int i;
+        unsigned int color_sets[2][2] = { { FOREGROUND, BACKGROUND }, { BACKGROUND, FOREGROUND } };
+        for (i = 0; i < 2; i++)
+          {
+            unsigned int color1 = color_sets[i][0];
+            unsigned int color2 = color_sets[i][1];
+            if (CHECK_COLORS_RANDOM (color1, color2))
+              vfprintf_fail (formats[FMT_RANDOM], tables[color1].desc, color_names[color1]->orig, "cannot be combined with", color_names[color2]->orig);
+          }
+      }
+
+    find_color_entries (color_names, colors);
+    free_color_names (color_names);
+
+    if (!colors[FOREGROUND]->code && colors[BACKGROUND] && colors[BACKGROUND]->code)
+      find_color_entry ("default", FOREGROUND, colors);
+
+    if (file_string)
+      {
+        if (streq (file_string, "-"))
+          *stream = stdin;
+        else
+          {
+            FILE *s;
+            const char *file = file_string;
+            struct stat sb;
+            int errno, ret;
+
+            errno = 0;
+            ret = stat (file, &sb);
+
+            if (ret == -1)
+              vfprintf_fail (formats[FMT_FILE], file, strerror (errno));
+
+            if (!(S_ISREG (sb.st_mode) || S_ISLNK (sb.st_mode) || S_ISFIFO (sb.st_mode)))
+              vfprintf_fail (formats[FMT_FILE], file, "unrecognized file type");
+
+            errno = 0;
+
+            s = fopen (file, "r");
+            if (!s)
+              vfprintf_fail (formats[FMT_FILE], file, strerror (errno));
+            *stream = s;
+          }
+        *file = file_string;
+      }
+    else
+      {
+        *stream = stdin;
+        *file = "stdin";
+      }
+
+    assert (*stream);
+}
+
+static void
+read_print_stream (bool bold, const struct color **colors, const char *file, FILE *stream, enum stream_mode mode)
+{
+    char buf[BUF_SIZE];
+    unsigned int flags = 0;
+    bool first = false, always = false;
+
+    switch (mode)
+      {
+        case SCAN_FIRST:
+          first = true;
+          break;
+        case SCAN_ALWAYS:
+          always = true;
+          break;
+        default: /* never reached */
+          ABORT_TRACE ();
+      }
+
+    while (!feof (stream))
+      {
+        size_t bytes_read;
+        char *eol;
+        const char *line;
+        memset (buf, '\0', BUF_SIZE);
+        bytes_read = fread (buf, 1, BUF_SIZE - 1, stream);
+        if (bytes_read != (BUF_SIZE - 1) && ferror (stream))
+          vfprintf_fail (formats[FMT_ERROR], BUF_SIZE - 1, "read");
+        line = buf;
+        LOOP: while ((eol = strpbrk (line, "\n\r")))
+          {
+            char *p;
+            if (first || always)
+              {
+                first = false;
+                flags &= ~(CR|LF);
+                if (*eol == '\r')
+                  {
+                    flags |= CR;
+                    if (*(eol + 1) == '\n')
+                      flags |= LF;
+                  }
+                else if (*eol == '\n')
+                  flags |= LF;
+                else
+                  vfprintf_fail (formats[FMT_FILE], file, "unrecognized line ending");
+              }
+            if (always)
+              p = eol + SKIP_LINE_ENDINGS (flags);
+            else /* first */
+              {
+                unsigned int i;
+                unsigned int count = sizeof (endings) / sizeof (struct ending);
+                for (i = 0; i < count; i++)
+                  {
+                    if (flags & endings[i].flags)
+                      {
+                        char *p;
+                        if ((p = strstr (eol, endings[i].newline)) && p == eol)
+                          break;
+                        else
+                          {
+                            always = true;
+                            goto LOOP;
+                          }
+                      }
+                  }
+                p = eol + SKIP_LINE_ENDINGS (flags);
+              }
+            *eol = '\0';
+            print_line (colors, bold, line, flags);
+            line = p;
+          }
+        print_line (colors, bold, line, 0);
+      }
+}
+
+static void
+find_color_entries (struct color_name **color_names, const struct color **colors)
+{
+    struct timeval tv;
+    unsigned int index;
+
+    /* randomness */
+    gettimeofday (&tv, NULL);
+    srand (tv.tv_usec * tv.tv_sec);
+
+    for (index = 0; color_names[index]; index++)
+      {
+        const char *color_name = color_names[index]->name;
+
+        const unsigned int count                = tables[index].count;
+        const struct color *const color_entries = tables[index].entries;
+
+        if (streq (color_name, "random"))
+          {
+            bool excludable;
+            unsigned int i;
+            do {
+              excludable = false;
+              i = rand() % (count - 2) + 1; /* omit color none and default */
+              switch (index)
+                {
+                  case FOREGROUND:
+                    /* --exclude-random */
+                    if (exclude && streq (exclude, color_entries[i].name))
+                      excludable = true;
+                    else if (color_names[BACKGROUND] && streq (color_names[BACKGROUND]->name, color_entries[i].name))
+                      excludable = true;
+                    break;
+                  case BACKGROUND:
+                    if (streq (colors[FOREGROUND]->name, color_entries[i].name))
+                      excludable = true;
+                    break;
+                  default: /* never reached */
+                    ABORT_TRACE ();
+                 }
+            } while (excludable);
+            colors[index] = (struct color *)&color_entries[i];
+          }
+        else
+          find_color_entry (color_name, index, colors);
+      }
+}
+
+static void
+find_color_entry (const char *const color_name, unsigned int index, const struct color **colors)
+{
+    bool found = false;
+    unsigned int i;
+
+    const unsigned int count                = tables[index].count;
+    const struct color *const color_entries = tables[index].entries;
+
+    for (i = 0; i < count; i++)
+      if (streq (color_name, color_entries[i].name))
+        {
+          colors[index] = (struct color *)&color_entries[i];
+          found = true;
+          break;
+        }
+    if (!found)
+      vfprintf_fail (formats[FMT_COLOR], tables[index].desc, color_name, "not recognized");
+}
+
+static void
+print_line (const struct color **colors, bool bold, const char *const line, unsigned int flags)
+{
+    if (colors[BACKGROUND] && colors[BACKGROUND]->code)
+      printf ("\033[%s", colors[BACKGROUND]->code);
+    if (colors[FOREGROUND]->code)
+      printf ("\033[%s%s%s\033[0m", bold ? "1;" : "", colors[FOREGROUND]->code, line);
+    else
+      printf (formats[FMT_GENERIC], line);
+    if (flags & CR)
+      putchar ('\r');
+    if (flags & LF)
+      putchar ('\n');
+}
+
+static char *
+xstrdup (const char *str)
+{
+    const unsigned int len = strlen (str) + 1;
+    char *p = malloc (len);
+    assert (p != NULL);
+    strncpy (p, str, len);
+    return p;
+}
+
+static void
+vfprintf_fail (const char *fmt, ...)
+{
+    va_list ap;
+    fprintf (stderr, "%s: ", program_name);
+    va_start (ap, fmt);
+    vfprintf (stderr, fmt, ap);
+    va_end (ap);
+    fprintf (stderr, "\n");
+    exit (EXIT_FAILURE);
+}
+
+static void
+stack_var (void ***list, unsigned int *stacked, unsigned int index, void *ptr)
+{
+    /* nothing to stack */
+    if (ptr == NULL)
+      return;
+    if (!*list)
+      *list = malloc (sizeof (void *));
+    else
+      {
+        unsigned int i;
+        for (i = 0; i < *stacked; i++)
+          if (!(*list)[i])
+            {
+              (*list)[i] = ptr;
+              return; /* reused */
+            }
+        *list = realloc (*list, (*stacked + 1) * sizeof (void *));
+      }
+    (*list)[index] = ptr;
+    (*stacked)++;
+}
+
+static void
+release_var (void **list, unsigned int stacked, void **ptr)
+{
+    unsigned int i;
+    /* nothing to release */
+    if (*ptr == NULL)
+      return;
+    for (i = 0; i < stacked; i++)
+      if (list[i] == *ptr)
+        {
+          free (*ptr);
+          *ptr = NULL;
+          list[i] = NULL;
+          return;
+        }
+}
diff --git a/test.pl b/test.pl
new file mode 100755 (executable)
index 0000000..d8fc52a
--- /dev/null
+++ b/test.pl
@@ -0,0 +1,91 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use constant true => 1;
+
+use File::Temp qw(tempfile tmpnam);
+use Test::More tests => 9;
+
+my $BUF_SIZE = 1024;
+my $source = 'colorize.c';
+my $warning_flags = '-Wall -Wextra -Wformat -Wswitch-default -Wuninitialized -Wunused';
+
+my $write_to_tmpfile = sub
+{
+    my ($content) = @_;
+
+    my ($fh, $tmpfile) = tempfile(UNLINK => true);
+    print {$fh} $content;
+    close($fh);
+
+    return $tmpfile;
+};
+
+my $program;
+
+SKIP: {
+    skip "$source does not exist", 9 unless -e $source;
+
+    $program = tmpnam();
+
+    skip 'compiling failed', 9 unless system("gcc -DTEST -DBUF_SIZE=$BUF_SIZE $warning_flags -o $program $source") == 0;
+
+    is(system("$program --help >/dev/null 2>&1"), 0, 'exit value for help screen');
+
+    is(qx(echo    "hello world" | $program none/none), "hello world\n", 'line read from stdin with newline');
+    is(qx(echo -n "hello world" | $program none/none), "hello world",   'line read from stdin without newline');
+
+    my $text = do { local $/; <DATA> };
+
+    my $infile1 = $write_to_tmpfile->($text);
+
+    is_deeply([split /\n/, qx(cat $infile1 | $program none/none)], [split /\n/, $text], 'text read from stdin');
+    is_deeply([split /\n/, qx($program none/none $infile1)],       [split /\n/, $text], 'text read from file');
+
+    my $repeated = join "\n", ($text) x 7;
+    my $infile2  = $write_to_tmpfile->($repeated);
+
+    is_deeply([split /\n/, qx(cat $infile2 | $program none/none)], [split /\n/, $repeated], "read ${\length $repeated} bytes (BUF_SIZE=$BUF_SIZE)");
+
+    is(qx(echo -n "hello\nworld\r\n" | $program none/none), "hello\nworld\r\n", 'stream mode');
+
+    is(system("echo \"hello world\" | $program random --exclude-random=black >/dev/null 2>&1"), 0, 'switch exclude-random');
+
+    skip 'valgrind not found', 1 unless system('which valgrind >/dev/null 2>&1') == 0;
+    like(qx(valgrind $program none/none $infile1 2>&1 >/dev/null), qr/no leaks are possible/, 'valgrind memleaks');
+
+    print <<'EOT';
+Colors
+======
+EOT
+    foreach my $color (qw(none black red green yellow blue cyan magenta white default random)) {
+        system("echo $color | $program $color");
+        next if $color eq 'none';
+        my $bold_color = ucfirst $color;
+        system("echo $bold_color | $program $bold_color");
+    }
+};
+
+unlink $program if defined $program;
+
+__DATA__
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus urna mauris, ultricies faucibus placerat sit amet, rutrum eu
+nisi. Quisque dictum turpis non augue iaculis tincidunt nec a arcu. Donec euismod sapien ac dui blandit et adipiscing risus
+semper. Sed ornare ligula magna, vitae molestie eros. Praesent ligula est, euismod a luctus non, porttitor quis nunc. Fusce vel
+imperdiet turpis. Proin vitae mauris neque, fringilla vestibulum sapien. Pellentesque vitae nibh ipsum, non cursus diam. Cras
+vitae ligula mauris. Etiam tortor enim, varius nec adipiscing sed, lobortis et quam. Quisque convallis, diam sagittis adipiscing
+adipiscing, mi nibh fermentum sapien, et iaculis nisi sem sit amet odio. Cras a tortor at nibh tristique vehicula dapibus eu velit.
+
+Vivamus porttitor purus eget leo suscipit sed posuere ligula gravida. In mollis velit quis leo pharetra gravida. Ut libero nisi,
+elementum sed varius tincidunt, hendrerit ut dui. Duis sit amet ante eget velit dictum ultrices. Nulla tempus, lacus eu dignissim
+feugiat, turpis mauris volutpat urna, quis commodo lorem augue id justo. Aenean consequat interdum sapien, sit amet
+imperdiet ante dapibus at. Pellentesque viverra sagittis tincidunt. Quisque rhoncus varius magna, sit amet rutrum arcu
+tincidunt eget. Etiam a lacus nec mauris interdum luctus sed in lacus. Ut pulvinar, augue at dictum blandit, nisl massa pretium
+ligula, in iaculis nulla nisi iaculis nunc.
+
+Vivamus id eros nunc. Cras facilisis iaculis ante sit amet consequat. Nunc vehicula imperdiet sem, ac vehicula neque
+condimentum sed. Phasellus metus lacus, molestie ullamcorper imperdiet in, condimentum ut tellus. Nullam dignissim dui ut
+enim ullamcorper in tempus risus posuere. Ut volutpat enim eleifend diam convallis tristique. Proin porttitor augue sed sapien
+sagittis quis facilisis purus sodales. Integer auctor dolor rhoncus nisl consequat adipiscing. Aliquam eget ante sit amet quam
+porta eleifend.