aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile20
-rw-r--r--colorize.c730
-rwxr-xr-xtest.pl91
3 files changed, 841 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
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
index 0000000..7e9edcb
--- /dev/null
+++ b/colorize.c
@@ -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
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.