Subject: Re: [bug-diffutils] bug#20062: bug#20062: bug#20062: bug#20062: bug#20062: [PATCH] diff: add support for --color

From: Giuseppe Scrivano <[email-masked]>
Date: Tue, 20 Oct 2015 18:23:06 +0200

Hi Jim,

Jim Meyering <[email-masked]> writes:

> Thank you for that patch and for your patience.

thank you very much for the review!

> I've skimmed through and so far have only a question and a request:
>
> Regarding this section:
>
> +void
> +set_header_color_context (void)
> +{
> +  process_signals ();
> +  if (colors_enabled)
> +    fprintf (outfile, "\x1B[1;39m");
> +}
> +
> +void
> +set_line_numbers_color_context (void)
> +{
> +  process_signals ();
> +  if (colors_enabled)
> +    fprintf (outfile, "\x1B[36m");
> +}
> +
> +void
> +set_add_color_context (void)
> +{
> +  process_signals ();
> +  if (colors_enabled)
> +    fprintf (outfile, "\x1B[32m");
> +  fflush (outfile);
> +}
> +
> +void
> +set_delete_color_context (void)
> +{
> +  process_signals ();
> +  if (colors_enabled)
> +    fprintf (outfile, "\x1B[31m");
> +}
> +
> +void
> +reset_color_context (void)
> +{
> +  static char const reset_sequence[] = "\x1b[0m";
> +  if (! colors_enabled)
> +    return;
> +
> +  fputs (reset_sequence, outfile);
> +}
>
> Why does set_add_color_context call fflush, yet the others do not?
> Please use fputs rather than fprintf for those literal strings.
> The former is often far more efficient.

Yes, it shouldn't as well.  I have changed it.

> Finally, should there be some way to specify different colors,
> e.g., for those who use different-background-colored terminals,
> or for the color blind?

I have took more code from coreutils ls and diff honors DIFF_COLORS
now.  I added it in a separate patch to facilitate the review.  Probably
all the shared code should end up in a gnulib module, but it probably
needs a better API before it can happen.

Changes in the first patch:

1) dropped fflush from set_add_color_context
2) use fputs instead of fprintf (the second patch replaces it)
3) change the code color of the header to the default color to match
the "git diff" output.

Regards,
Giuseppe

>From 581c602ef651f99418a086025ad6a035959a7ad7 Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Sun, 8 Mar 2015 22:45:11 +0100
Subject: [PATCH 1/2] diff: add support for --color

* doc/diffutils.texi (diff Options): Add documentation for --color.
Copied from coreutils ls --color.
* src/context.c (pr_unidiff_hunk): Set the color context.
(print_context_header): Likewise.
(pr_context_hunk): Likewise.
* src/diff.h (enum colors_style): New enum to record when to use colors.
(colors_style): New variable to memorize the argument value.
(set_add_color_context): Add function definition.
(set_delete_color_context): Likewise.
(set_header_color_context): Likewise.
(set_line_numbers_color_context): Likewise.
(reset_color_context): Likewise.
* src/diff.c: : Define COLOR_OPTION.
(specify_colors_style): New function.
(longopts): Add --color.
(main): Handle --color argument.
(option_help_msgid): Add usage string for --color.
* src/normal.c (print_normal_hunk): Set the color context.
* src/side.c (print_1sdiff_line): Likewise.
* src/util.c (colors_enabled): New boolean variable.
(begin_output): Call check_color_output once the output file is
configured.
(output_1_line): Periodically call `process_signals'.
(caught_signals): New sigset_t.
(colors_enabled): New boolean variable.
(interrupt_signal): New sig_atomic_t.
(stop_signal_count): New sig_atomic_t.
(check_color_output): New function.
(install_signal_handlers): Likewise. Copied from coreutils ls.
(process_signals): Likewise.  Copied from coreutils ls.
(reset_color_context): Likewise.
(set_add_color_context): Likewise.
(set_delete_color_context): Likewise.
(set_header_color_context): Likewise.
(set_line_numbers_color_context): Likewise.
(sighandler): Likewise.  Copied from coreutils ls.
(stophandler): Likewise.  Copied from coreutils ls.
---
 doc/diffutils.texi |  21 ++++
 src/context.c      |  51 +++++++--
 src/diff.c         |  27 ++++-
 src/diff.h         |  21 ++++
 src/normal.c       |  18 ++-
 src/side.c         |  15 +++
 src/util.c         | 316 +++++++++++++++++++++++++++++++++++++++++++++++------
 7 files changed, 421 insertions(+), 48 deletions(-)

diff --git a/doc/diffutils.texi b/doc/diffutils.texi
index 091257f..0944b44 100644
--- a/doc/diffutils.texi
+++ b/doc/diffutils.texi
@@ -3742,6 +3742,27 @@ Read and write data in binary mode.  @xref{Binary}.
 Use the context output format, showing three lines of context.
 @xref{Context Format}.
 
+@item --color [=@var{when}]
+@cindex color, distinguishing different context
+Specify whether to use color for distinguishing different contexts,
+like header, added or removed lines.  @var{when} may be omitted, or
+one of:
+@itemize @bullet
+@item none
+@vindex none @r{color option}
+Do not use color at all.  This is the default when no --color option
+is present.
+@item auto
+@vindex auto @r{color option}
+@cindex terminal, using color iff
+Only use color if standard output is a terminal.
+@item always
+@vindex always @r{color option}
+Always use color.
+@end itemize
+Specifying @option{--color} and no @var{when} is equivalent to
+@option{--color=auto}.
+
 @item -C @var{lines}
 @itemx --context@r{[}=@var{lines}@r{]}
 Use the context output format, showing @var{lines} (an integer) lines of
diff --git a/src/context.c b/src/context.c
index e0f21c4..8e9a74f 100644
--- a/src/context.c
+++ b/src/context.c
@@ -80,6 +80,7 @@ print_context_label (char const *mark,
 void
 print_context_header (struct file_data inf[], char const *const *names, bool unidiff)
 {
+  set_header_color_context ();
   if (unidiff)
     {
       print_context_label ("---", &inf[0], names[0], file_label[0]);
@@ -90,6 +91,7 @@ print_context_header (struct file_data inf[], char const *const *names, bool uni
       print_context_label ("***", &inf[0], names[0], file_label[0]);
       print_context_label ("---", &inf[1], names[1], file_label[1]);
     }
+  reset_color_context ();
 }
 
 /* Print an edit script in context format.  */
@@ -215,6 +217,7 @@ pr_context_hunk (struct change *hunk)
 
       for (i = first0; i <= last0; i++)
 	{
+          bool reset_context = false;
 	  /* Skip past changes that apply (in file 0)
 	     only to lines before line I.  */
 
@@ -225,12 +228,18 @@ pr_context_hunk (struct change *hunk)
 
 	  prefix = " ";
 	  if (next && next->line0 <= i)
-	    /* The change NEXT covers this line.
-	       If lines were inserted here in file 1, this is "changed".
-	       Otherwise it is "deleted".  */
-	    prefix = (next->inserted > 0 ? "!" : "-");
+            {
+              reset_context = true;
+              set_delete_color_context ();
+              /* The change NEXT covers this line.
+                 If lines were inserted here in file 1, this is "changed".
+                 Otherwise it is "deleted".  */
+              prefix = (next->inserted > 0 ? "!" : "-");
+            }
 
 	  print_1_line (prefix, &files[0].linbuf[i]);
+          if (reset_context)
+            reset_color_context ();
 	}
     }
 
@@ -244,6 +253,7 @@ pr_context_hunk (struct change *hunk)
 
       for (i = first1; i <= last1; i++)
 	{
+          bool reset_context = false;
 	  /* Skip past changes that apply (in file 1)
 	     only to lines before line I.  */
 
@@ -254,12 +264,17 @@ pr_context_hunk (struct change *hunk)
 
 	  prefix = " ";
 	  if (next && next->line1 <= i)
-	    /* The change NEXT covers this line.
-	       If lines were deleted here in file 0, this is "changed".
-	       Otherwise it is "inserted".  */
-	    prefix = (next->deleted > 0 ? "!" : "+");
-
+            {
+              reset_context = true;
+              set_add_color_context ();
+              /* The change NEXT covers this line.
+                 If lines were deleted here in file 0, this is "changed".
+                 Otherwise it is "inserted".  */
+              prefix = (next->deleted > 0 ? "!" : "+");
+            }
 	  print_1_line (prefix, &files[1].linbuf[i]);
+          if (reset_context)
+            reset_color_context ();
 	}
     }
 }
@@ -330,11 +345,13 @@ pr_unidiff_hunk (struct change *hunk)
   begin_output ();
   out = outfile;
 
+  set_line_numbers_color_context ();
   fputs ("@@ -", out);
   print_unidiff_number_range (&files[0], first0, last0);
   fputs (" +", out);
   print_unidiff_number_range (&files[1], first1, last1);
   fputs (" @@", out);
+  reset_color_context ();
 
   if (function)
     print_context_function (out, function);
@@ -360,9 +377,17 @@ pr_unidiff_hunk (struct change *hunk)
 	}
       else
 	{
+          bool reset_context = false;
+
 	  /* For each difference, first output the deleted part. */
 
 	  k = next->deleted;
+          if (k)
+            {
+              reset_context = true;
+              set_delete_color_context ();
+            }
+
 	  while (k--)
 	    {
 	      char const * const *line = &files[0].linbuf[i++];
@@ -375,9 +400,15 @@ pr_unidiff_hunk (struct change *hunk)
 	  /* Then output the inserted part. */
 
 	  k = next->inserted;
+          if (k)
+            {
+              reset_context = true;
+              set_add_color_context ();
+            }
 	  while (k--)
 	    {
 	      char const * const *line = &files[1].linbuf[j++];
+	      set_add_color_context ();
 	      putc ('+', out);
 	      if (initial_tab && ! (suppress_blank_empty && **line == '\n'))
 		putc ('\t', out);
@@ -386,6 +417,8 @@ pr_unidiff_hunk (struct change *hunk)
 
 	  /* We're done with this hunk, so on to the next! */
 
+          if (reset_context)
+            reset_color_context ();
 	  next = next->link;
 	}
     }
diff --git a/src/diff.c b/src/diff.c
index efd7e47..4e0f602 100644
--- a/src/diff.c
+++ b/src/diff.c
@@ -70,6 +70,7 @@ static void add_regexp (struct regexp_list *, char const *);
 static void summarize_regexp_list (struct regexp_list *);
 static void specify_style (enum output_style);
 static void specify_value (char const **, char const *, char const *);
+static void specify_colors_style (char const *);
 static void try_help (char const *, char const *) __attribute__((noreturn));
 static void check_stdout (void);
 static void usage (void);
@@ -136,7 +137,9 @@ enum
   UNCHANGED_GROUP_FORMAT_OPTION,
   OLD_GROUP_FORMAT_OPTION,
   NEW_GROUP_FORMAT_OPTION,
-  CHANGED_GROUP_FORMAT_OPTION
+  CHANGED_GROUP_FORMAT_OPTION,
+
+  COLOR_OPTION,
 };
 
 static char const group_format_option[][sizeof "--unchanged-group-format"] =
@@ -159,6 +162,7 @@ static struct option const longopts[] =
   {"binary", 0, 0, BINARY_OPTION},
   {"brief", 0, 0, 'q'},
   {"changed-group-format", 1, 0, CHANGED_GROUP_FORMAT_OPTION},
+  {"color", 2, 0, COLOR_OPTION},
   {"context", 2, 0, 'C'},
   {"ed", 0, 0, 'e'},
   {"exclude", 1, 0, 'x'},
@@ -627,6 +631,10 @@ main (int argc, char **argv)
 	  specify_value (&group_format[c], optarg, group_format_option[c]);
 	  break;
 
+	case COLOR_OPTION:
+	  specify_colors_style (optarg);
+	  break;
+
 	default:
 	  try_help (NULL, NULL);
 	}
@@ -940,6 +948,8 @@ static char const * const option_help_msgid[] = {
   N_("-d, --minimal            try hard to find a smaller set of changes"),
   N_("    --horizon-lines=NUM  keep NUM lines of the common prefix and suffix"),
   N_("    --speed-large-files  assume large files and many scattered small changes"),
+  N_("    --color[=WHEN]         colorize the output; WHEN can be 'never', 'always',"),
+  N_("                             or 'auto' (the default)"),
   "",
   N_("    --help               display this help and exit"),
   N_("-v, --version            output version information and exit"),
@@ -1008,6 +1018,21 @@ specify_style (enum output_style style)
       output_style = style;
     }
 }
+
+/* Set the color mode.  */
+static void
+specify_colors_style (char const *value)
+{
+  if (value == NULL || STREQ (value, "auto"))
+    colors_style = AUTO;
+  else if (STREQ (value, "always"))
+    colors_style = ALWAYS;
+  else if (STREQ (value, "never"))
+    colors_style = NEVER;
+  else
+    try_help ("invalid color '%s'", value);
+}
+
 
 /* Set the last-modified time of *ST to be the current time.  */
 
diff --git a/src/diff.h b/src/diff.h
index 465e4bc..472fa93 100644
--- a/src/diff.h
+++ b/src/diff.h
@@ -38,6 +38,19 @@ enum changes
   /* Both deletes and inserts: a hunk containing both old and new lines.  */
   CHANGED
 };
+
+/* What kind of changes a hunk contains.  */
+enum colors_style
+{
+  /* Never output colors.  */
+  NEVER,
+
+  /* Output colors if the output is a terminal.  */
+  AUTO,
+
+  /* Always output colors.  */
+  ALWAYS,
+};
 
 /* Variables for command line options */
 
@@ -83,6 +96,9 @@ enum output_style
 
 XTERN enum output_style output_style;
 
+/* Define the current color context used to print a line.  */
+XTERN enum colors_style colors_style;
+
 /* Nonzero if output cannot be generated for identical files.  */
 XTERN bool no_diff_means_no_output;
 
@@ -390,3 +406,8 @@ extern void print_script (struct change *, struct change * (*) (struct change *)
 extern void setup_output (char const *, char const *, bool);
 extern void translate_range (struct file_data const *, lin, lin,
                              long int *, long int *);
+extern void set_header_color_context (void);
+extern void set_add_color_context (void);
+extern void set_delete_color_context (void);
+extern void reset_color_context (void);
+extern void set_line_numbers_color_context (void);
diff --git a/src/normal.c b/src/normal.c
index 721fd1a..227af10 100644
--- a/src/normal.c
+++ b/src/normal.c
@@ -49,21 +49,31 @@ print_normal_hunk (struct change *hunk)
   begin_output ();
 
   /* Print out the line number header for this hunk */
+  set_line_numbers_color_context ();
   print_number_range (',', &files[0], first0, last0);
   fputc (change_letter[changes], outfile);
   print_number_range (',', &files[1], first1, last1);
   fputc ('\n', outfile);
+  reset_color_context ();
 
   /* Print the lines that the first file has.  */
   if (changes & OLD)
-    for (i = first0; i <= last0; i++)
-      print_1_line ("<", &files[0].linbuf[i]);
+    {
+      set_delete_color_context ();
+      for (i = first0; i <= last0; i++)
+        print_1_line ("<", &files[0].linbuf[i]);
+      reset_color_context ();
+    }
 
   if (changes == CHANGED)
     fputs ("---\n", outfile);
 
   /* Print the lines that the second file has.  */
   if (changes & NEW)
-    for (i = first1; i <= last1; i++)
-      print_1_line (">", &files[1].linbuf[i]);
+    {
+      set_add_color_context ();
+      for (i = first1; i <= last1; i++)
+        print_1_line (">", &files[1].linbuf[i]);
+      reset_color_context ();
+    }
 }
diff --git a/src/side.c b/src/side.c
index 155512c..b762d31 100644
--- a/src/side.c
+++ b/src/side.c
@@ -206,6 +206,18 @@ print_1sdiff_line (char const *const *left, char sep,
   size_t c2o = sdiff_column2_offset;
   size_t col = 0;
   bool put_newline = false;
+  bool color_to_reset = false;
+
+  if (sep == '<')
+    {
+      set_delete_color_context ();
+      color_to_reset = true;
+    }
+  else if (sep == '>')
+    {
+      set_add_color_context ();
+      color_to_reset = true;
+    }
 
   if (left)
     {
@@ -233,6 +245,9 @@ print_1sdiff_line (char const *const *left, char sep,
 
   if (put_newline)
     putc ('\n', out);
+
+  if (color_to_reset)
+    reset_color_context ();
 }
 
 /* Print lines common to both files in side-by-side format.  */
diff --git a/src/util.c b/src/util.c
index 2d6d3fc..6cc1411 100644
--- a/src/util.c
+++ b/src/util.c
@@ -24,6 +24,22 @@
 #include <system-quote.h>
 #include <xalloc.h>
 #include "xvasprintf.h"
+#include <signal.h>
+
+/* Use SA_NOCLDSTOP as a proxy for whether the sigaction machinery is
+   present.  */
+#ifndef SA_NOCLDSTOP
+# define SA_NOCLDSTOP 0
+# define sigprocmask(How, Set, Oset) /* empty */
+# define sigset_t int
+# if ! HAVE_SIGINTERRUPT
+#  define siginterrupt(sig, flag) /* empty */
+# endif
+#endif
+
+#ifndef SA_RESTART
+# define SA_RESTART 0
+#endif
 
 char const pr_program[] = PR_PROGRAM;
 
@@ -143,6 +159,174 @@ print_message_queue (void)
     }
 }
 
+/* The set of signals that are caught.  */
+
+static sigset_t caught_signals;
+
+/* If nonzero, the value of the pending fatal signal.  */
+
+static sig_atomic_t volatile interrupt_signal;
+
+/* A count of the number of pending stop signals that have been received.  */
+
+static sig_atomic_t volatile stop_signal_count;
+
+/* An ordinary signal was received; arrange for the program to exit.  */
+
+static void
+sighandler (int sig)
+{
+  if (! SA_NOCLDSTOP)
+    signal (sig, SIG_IGN);
+  if (! interrupt_signal)
+    interrupt_signal = sig;
+}
+
+/* A SIGTSTP was received; arrange for the program to suspend itself.  */
+
+static void
+stophandler (int sig)
+{
+  if (! SA_NOCLDSTOP)
+    signal (sig, stophandler);
+  if (! interrupt_signal)
+    stop_signal_count++;
+}
+/* Process any pending signals.  If signals are caught, this function
+   should be called periodically.  Ideally there should never be an
+   unbounded amount of time when signals are not being processed.
+   Signal handling can restore the default colors, so callers must
+   immediately change colors after invoking this function.  */
+
+static void
+process_signals (void)
+{
+  while (interrupt_signal || stop_signal_count)
+    {
+      int sig;
+      int stops;
+      sigset_t oldset;
+
+      reset_color_context ();
+      fflush (stdout);
+
+      sigprocmask (SIG_BLOCK, &caught_signals, &oldset);
+
+      /* Reload interrupt_signal and stop_signal_count, in case a new
+         signal was handled before sigprocmask took effect.  */
+      sig = interrupt_signal;
+      stops = stop_signal_count;
+
+      /* SIGTSTP is special, since the application can receive that signal
+         more than once.  In this case, don't set the signal handler to the
+         default.  Instead, just raise the uncatchable SIGSTOP.  */
+      if (stops)
+        {
+          stop_signal_count = stops - 1;
+          sig = SIGSTOP;
+        }
+      else
+        signal (sig, SIG_DFL);
+
+      /* Exit or suspend the program.  */
+      raise (sig);
+      sigprocmask (SIG_SETMASK, &oldset, NULL);
+
+      /* If execution reaches here, then the program has been
+         continued (after being suspended).  */
+    }
+}
+
+static void
+install_signal_handlers (void)
+{
+  /* The signals that are trapped, and the number of such signals.  */
+  static int const sig[] =
+    {
+      /* This one is handled specially.  */
+      SIGTSTP,
+
+      /* The usual suspects.  */
+      SIGALRM, SIGHUP, SIGINT, SIGPIPE, SIGQUIT, SIGTERM,
+#ifdef SIGPOLL
+      SIGPOLL,
+#endif
+#ifdef SIGPROF
+      SIGPROF,
+#endif
+#ifdef SIGVTALRM
+      SIGVTALRM,
+#endif
+#ifdef SIGXCPU
+      SIGXCPU,
+#endif
+#ifdef SIGXFSZ
+      SIGXFSZ,
+#endif
+    };
+  enum { nsigs = sizeof (sig) / sizeof *(sig) };
+
+#if ! SA_NOCLDSTOP
+  bool caught_sig[nsigs];
+#endif
+  {
+    int j;
+#if SA_NOCLDSTOP
+    struct sigaction act;
+
+    sigemptyset (&caught_signals);
+    for (j = 0; j < nsigs; j++)
+      {
+        sigaction (sig[j], NULL, &act);
+        if (act.sa_handler != SIG_IGN)
+          sigaddset (&caught_signals, sig[j]);
+      }
+
+    act.sa_mask = caught_signals;
+    act.sa_flags = SA_RESTART;
+
+    for (j = 0; j < nsigs; j++)
+      if (sigismember (&caught_signals, sig[j]))
+        {
+          act.sa_handler = sig[j] == SIGTSTP ? stophandler : sighandler;
+          sigaction (sig[j], &act, NULL);
+        }
+#else
+    for (j = 0; j < nsigs; j++)
+      {
+        caught_sig[j] = (signal (sig[j], SIG_IGN) != SIG_IGN);
+        if (caught_sig[j])
+          {
+            signal (sig[j], sig[j] == SIGTSTP ? stophandler : sighandler);
+            siginterrupt (sig[j], 0);
+          }
+      }
+#endif
+    }
+}
+
+static char const *current_name0;
+static char const *current_name1;
+static bool currently_recursive;
+static bool colors_enabled;
+
+static void
+check_color_output (bool is_pipe)
+{
+  bool output_is_tty;
+
+  if (! outfile || colors_style == NEVER)
+    return;
+
+  output_is_tty = !is_pipe && isatty (fileno (outfile));
+
+  colors_enabled = (colors_style == ALWAYS
+                    || (colors_style == AUTO && output_is_tty));
+
+  if (output_is_tty)
+    install_signal_handlers ();
+}
+
 /* Call before outputting the results of comparing files NAME0 and NAME1
    to set up OUTFILE, the stdio stream for the output to go to.
 
@@ -150,10 +334,6 @@ print_message_queue (void)
    we fork off a 'pr' and make OUTFILE a pipe to it.
    'pr' then outputs to our stdout.  */
 
-static char const *current_name0;
-static char const *current_name1;
-static bool currently_recursive;
-
 void
 setup_output (char const *name0, char const *name1, bool recursive)
 {
@@ -313,6 +493,7 @@ begin_output (void)
 	    outfile = fdopen (pipes[1], "w");
 	    if (!outfile)
 	      pfatal_with_name ("fdopen");
+	    check_color_output (true);
 	  }
 #else
 	char *command = system_quote_argv (SCI_SYSTEM, (char **) argv);
@@ -320,6 +501,7 @@ begin_output (void)
 	outfile = popen (command, "w");
 	if (!outfile)
 	  pfatal_with_name (command);
+	check_color_output (true);
 	free (command);
 #endif
       }
@@ -330,6 +512,7 @@ begin_output (void)
       /* If -l was not specified, output the diff straight to 'stdout'.  */
 
       outfile = stdout;
+      check_color_output (false);
 
       /* If handling multiple files (because scanning a directory),
 	 print which files the following output is about.  */
@@ -672,8 +855,21 @@ void
 output_1_line (char const *base, char const *limit, char const *flag_format,
 	       char const *line_flag)
 {
+  const size_t MAX_CHUNK = 1024;
   if (!expand_tabs)
-    fwrite (base, sizeof (char), limit - base, outfile);
+    {
+      size_t left = limit - base;
+      while (left)
+        {
+          size_t to_write = MIN (left, MAX_CHUNK);
+          size_t written = fwrite (base, sizeof (char), to_write, outfile);
+          if (written < to_write)
+            return;
+          base += written;
+          left -= written;
+          process_signals ();
+        }
+    }
   else
     {
       register FILE *out = outfile;
@@ -681,40 +877,92 @@ output_1_line (char const *base, char const *limit, char const *flag_format,
       register char const *t = base;
       register size_t column = 0;
       size_t tab_size = tabsize;
+      size_t counter_proc_signals = 0;
 
       while (t < limit)
-	switch ((c = *t++))
-	  {
-	  case '\t':
-	    {
-	      size_t spaces = tab_size - column % tab_size;
-	      column += spaces;
-	      do
-		putc (' ', out);
-	      while (--spaces);
-	    }
-	    break;
+        {
+          counter_proc_signals++;
+          if (counter_proc_signals == MAX_CHUNK)
+            {
+              process_signals ();
+              counter_proc_signals = 0;
+            }
+
+          switch ((c = *t++))
+            {
+            case '\t':
+              {
+                size_t spaces = tab_size - column % tab_size;
+                column += spaces;
+                do
+                  putc (' ', out);
+                while (--spaces);
+              }
+              break;
+
+            case '\r':
+              putc (c, out);
+              if (flag_format && t < limit && *t != '\n')
+                fprintf (out, flag_format, line_flag);
+              column = 0;
+              break;
+
+            case '\b':
+              if (column == 0)
+                continue;
+              column--;
+              putc (c, out);
+              break;
+
+            default:
+              column += isprint (c) != 0;
+              putc (c, out);
+              break;
+            }
+        }
+    }
+}
 
-	  case '\r':
-	    putc (c, out);
-	    if (flag_format && t < limit && *t != '\n')
-	      fprintf (out, flag_format, line_flag);
-	    column = 0;
-	    break;
+void
+set_header_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[1m", outfile);
+}
 
-	  case '\b':
-	    if (column == 0)
-	      continue;
-	    column--;
-	    putc (c, out);
-	    break;
+void
+set_line_numbers_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[36m", outfile);
+}
 
-	  default:
-	    column += isprint (c) != 0;
-	    putc (c, out);
-	    break;
-	  }
-    }
+void
+set_add_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[32m", outfile);
+}
+
+void
+set_delete_color_context (void)
+{
+  process_signals ();
+  if (colors_enabled)
+    fputs ("\x1B[31m", outfile);
+}
+
+void
+reset_color_context (void)
+{
+  static char const reset_sequence[] = "\x1b[0m";
+  if (! colors_enabled)
+    return;
+
+  fputs (reset_sequence, outfile);
 }
 
 char const change_letter[] = { 0, 'd', 'a', 'c' };
-- 
2.4.3

>From 2e2e078878d04311791c2facd9cac79ec969f24d Mon Sep 17 00:00:00 2001
From: Giuseppe Scrivano <[email-masked]>
Date: Mon, 19 Oct 2015 10:29:41 +0200
Subject: [PATCH 2/2] diff: honor env variable DIFF_COLORS

doc/diffutils.texi: Add documentation for DIFF_COLORS
src/utils.c (struct bin_str): New struct.
(struct color_ext_type): New struct.
(color_indicator): New array.
(indicator_name): New array.
(indicator_no): New enum.
(parse_state): New enum.
(put_indicator): New function.
(get_funky_string): New function. Copied from coreutils ls.
(parse_diff_color):  New function. Copied from coreutils ls
"parse_ls_color" function.
(set_header_color_context): Use put_indicator instead of directly
outputting the sequence.
(set_line_numbers_colors_context): Likewise.
(set_add_color_context): Likewise.
(set_delete_color_context): Likewise.
(reset_color_context): Likewise.
---
 doc/diffutils.texi |   5 +
 src/util.c         | 431 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 427 insertions(+), 9 deletions(-)

diff --git a/doc/diffutils.texi b/doc/diffutils.texi
index 0944b44..75ad0d7 100644
--- a/doc/diffutils.texi
+++ b/doc/diffutils.texi
@@ -3763,6 +3763,11 @@ Always use color.
 Specifying @option{--color} and no @var{when} is equivalent to
 @option{--color=auto}.
 
+The colors are defined by the environment variable @env{DIFF_COLORS}
+and default to @samp{rs=0:hd=1:ad=32:de=31:ln=36}
+for red added lines, green deleted lines, cyan line numbers, bold header.
+
+
 @item -C @var{lines}
 @itemx --context@r{[}=@var{lines}@r{]}
 Use the context output format, showing @var{lines} (an integer) lines of
diff --git a/src/util.c b/src/util.c
index 6cc1411..8d27bc7 100644
--- a/src/util.c
+++ b/src/util.c
@@ -310,6 +310,388 @@ static char const *current_name1;
 static bool currently_recursive;
 static bool colors_enabled;
 
+static struct color_ext_type *color_ext_list = NULL;
+
+struct bin_str
+  {
+    size_t len;			/* Number of bytes */
+    const char *string;		/* Pointer to the same */
+  };
+
+struct color_ext_type
+  {
+    struct bin_str ext;		/* The extension we're looking for */
+    struct bin_str seq;		/* The sequence to output when we do */
+    struct color_ext_type *next;	/* Next in list */
+  };
+
+/* Parse a string as part of the DIFF_COLORS variable; this may involve
+   decoding all kinds of escape characters.  If equals_end is set an
+   unescaped equal sign ends the string, otherwise only a : or \0
+   does.  Set *OUTPUT_COUNT to the number of bytes output.  Return
+   true if successful.
+
+   The resulting string is *not* null-terminated, but may contain
+   embedded nulls.
+
+   Note that both dest and src are char **; on return they point to
+   the first free byte after the array and the character that ended
+   the input string, respectively.  */
+
+static bool
+get_funky_string (char **dest, const char **src, bool equals_end,
+                  size_t *output_count)
+{
+  char num;			/* For numerical codes */
+  size_t count;			/* Something to count with */
+  enum {
+    ST_GND, ST_BACKSLASH, ST_OCTAL, ST_HEX, ST_CARET, ST_END, ST_ERROR
+  } state;
+  const char *p;
+  char *q;
+
+  p = *src;			/* We don't want to double-indirect */
+  q = *dest;			/* the whole darn time.  */
+
+  count = 0;			/* No characters counted in yet.  */
+  num = 0;
+
+  state = ST_GND;		/* Start in ground state.  */
+  while (state < ST_END)
+    {
+      switch (state)
+        {
+        case ST_GND:		/* Ground state (no escapes) */
+          switch (*p)
+            {
+            case ':':
+            case '\0':
+              state = ST_END;	/* End of string */
+              break;
+            case '\\':
+              state = ST_BACKSLASH; /* Backslash scape sequence */
+              ++p;
+              break;
+            case '^':
+              state = ST_CARET; /* Caret escape */
+              ++p;
+              break;
+            case '=':
+              if (equals_end)
+                {
+                  state = ST_END; /* End */
+                  break;
+                }
+              /* else fall through */
+            default:
+              *(q++) = *(p++);
+              ++count;
+              break;
+            }
+          break;
+
+        case ST_BACKSLASH:	/* Backslash escaped character */
+          switch (*p)
+            {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+              state = ST_OCTAL;	/* Octal sequence */
+              num = *p - '0';
+              break;
+            case 'x':
+            case 'X':
+              state = ST_HEX;	/* Hex sequence */
+              num = 0;
+              break;
+            case 'a':		/* Bell */
+              num = '\a';
+              break;
+            case 'b':		/* Backspace */
+              num = '\b';
+              break;
+            case 'e':		/* Escape */
+              num = 27;
+              break;
+            case 'f':		/* Form feed */
+              num = '\f';
+              break;
+            case 'n':		/* Newline */
+              num = '\n';
+              break;
+            case 'r':		/* Carriage return */
+              num = '\r';
+              break;
+            case 't':		/* Tab */
+              num = '\t';
+              break;
+            case 'v':		/* Vtab */
+              num = '\v';
+              break;
+            case '?':		/* Delete */
+              num = 127;
+              break;
+            case '_':		/* Space */
+              num = ' ';
+              break;
+            case '\0':		/* End of string */
+              state = ST_ERROR;	/* Error! */
+              break;
+            default:		/* Escaped character like \ ^ : = */
+              num = *p;
+              break;
+            }
+          if (state == ST_BACKSLASH)
+            {
+              *(q++) = num;
+              ++count;
+              state = ST_GND;
+            }
+          ++p;
+          break;
+
+        case ST_OCTAL:		/* Octal sequence */
+          if (*p < '0' || *p > '7')
+            {
+              *(q++) = num;
+              ++count;
+              state = ST_GND;
+            }
+          else
+            num = (num << 3) + (*(p++) - '0');
+          break;
+
+        case ST_HEX:		/* Hex sequence */
+          switch (*p)
+            {
+            case '0':
+            case '1':
+            case '2':
+            case '3':
+            case '4':
+            case '5':
+            case '6':
+            case '7':
+            case '8':
+            case '9':
+              num = (num << 4) + (*(p++) - '0');
+              break;
+            case 'a':
+            case 'b':
+            case 'c':
+            case 'd':
+            case 'e':
+            case 'f':
+              num = (num << 4) + (*(p++) - 'a') + 10;
+              break;
+            case 'A':
+            case 'B':
+            case 'C':
+            case 'D':
+            case 'E':
+            case 'F':
+              num = (num << 4) + (*(p++) - 'A') + 10;
+              break;
+            default:
+              *(q++) = num;
+              ++count;
+              state = ST_GND;
+              break;
+            }
+          break;
+
+        case ST_CARET:		/* Caret escape */
+          state = ST_GND;	/* Should be the next state... */
+          if (*p >= '@' && *p <= '~')
+            {
+              *(q++) = *(p++) & 037;
+              ++count;
+            }
+          else if (*p == '?')
+            {
+              *(q++) = 127;
+              ++count;
+            }
+          else
+            state = ST_ERROR;
+          break;
+
+        default:
+          abort ();
+        }
+    }
+
+  *dest = q;
+  *src = p;
+  *output_count = count;
+
+  return state != ST_ERROR;
+}
+
+enum parse_state
+  {
+    PS_START = 1,
+    PS_2,
+    PS_3,
+    PS_4,
+    PS_DONE,
+    PS_FAIL
+  };
+
+#define LEN_STR_PAIR(s) sizeof (s) - 1, s
+
+static struct bin_str color_indicator[] =
+  {
+    { LEN_STR_PAIR ("\033[") },		/* lc: Left of color sequence */
+    { LEN_STR_PAIR ("m") },		/* rc: Right of color sequence */
+    { 0, NULL },			/* ec: End color (replaces lc+rs+rc) */
+    { LEN_STR_PAIR ("0") },		/* rs: Reset to ordinary colors */
+    { LEN_STR_PAIR ("1") },		/* hd: Header */
+    { LEN_STR_PAIR ("32") },		/* ad: Add line */
+    { LEN_STR_PAIR ("31") },		/* de: Delete line */
+    { LEN_STR_PAIR ("36") },		/* ln: Line number */
+  };
+
+static const char *const indicator_name[]=
+  {
+    "lc", "rc", "ec", "rs", "hd", "ad", "de", "ln", NULL
+  };
+
+static void
+parse_diff_color (void)
+{
+  char *color_buf;
+  const char *p;		/* Pointer to character being parsed */
+  char *buf;			/* color_buf buffer pointer */
+  int ind_no;			/* Indicator number */
+  char label[3];		/* Indicator label */
+  struct color_ext_type *ext;	/* Extension we are working on */
+
+  if ((p = getenv ("DIFF_COLORS")) == NULL || *p == '\0')
+    return;
+
+  ext = NULL;
+  strcpy (label, "??");
+
+  /* This is an overly conservative estimate, but any possible
+     DIFF_COLORS string will *not* generate a color_buf longer than
+     itself, so it is a safe way of allocating a buffer in
+     advance.  */
+  buf = color_buf = xstrdup (p);
+
+  enum parse_state state = PS_START;
+  while (true)
+    {
+      switch (state)
+        {
+        case PS_START:		/* First label character */
+          switch (*p)
+            {
+            case ':':
+              ++p;
+              break;
+
+            case '*':
+              /* Allocate new extension block and add to head of
+                 linked list (this way a later definition will
+                 override an earlier one, which can be useful for
+                 having terminal-specific defs override global).  */
+
+              ext = xmalloc (sizeof *ext);
+              ext->next = color_ext_list;
+              color_ext_list = ext;
+
+              ++p;
+              ext->ext.string = buf;
+
+              state = (get_funky_string (&buf, &p, true, &ext->ext.len)
+                       ? PS_4 : PS_FAIL);
+              break;
+
+            case '\0':
+              state = PS_DONE;	/* Done! */
+              goto done;
+
+            default:	/* Assume it is file type label */
+              label[0] = *(p++);
+              state = PS_2;
+              break;
+            }
+          break;
+
+        case PS_2:		/* Second label character */
+          if (*p)
+            {
+              label[1] = *(p++);
+              state = PS_3;
+            }
+          else
+            state = PS_FAIL;	/* Error */
+          break;
+
+        case PS_3:		/* Equal sign after indicator label */
+          state = PS_FAIL;	/* Assume failure...  */
+          if (*(p++) == '=')/* It *should* be...  */
+            {
+              for (ind_no = 0; indicator_name[ind_no] != NULL; ++ind_no)
+                {
+                  if (STREQ (label, indicator_name[ind_no]))
+                    {
+                      color_indicator[ind_no].string = buf;
+                      state = (get_funky_string (&buf, &p, false,
+                                                 &color_indicator[ind_no].len)
+                               ? PS_START : PS_FAIL);
+                      break;
+                    }
+                }
+              if (state == PS_FAIL)
+                error (0, 0, _("unrecognized prefix: %s"), label);
+            }
+          break;
+
+        case PS_4:		/* Equal sign after *.ext */
+          if (*(p++) == '=')
+            {
+              ext->seq.string = buf;
+              state = (get_funky_string (&buf, &p, false, &ext->seq.len)
+                       ? PS_START : PS_FAIL);
+            }
+          else
+            state = PS_FAIL;
+          break;
+
+        case PS_FAIL:
+          goto done;
+
+        default:
+          abort ();
+        }
+    }
+ done:
+
+  if (state == PS_FAIL)
+    {
+      struct color_ext_type *e;
+      struct color_ext_type *e2;
+
+      error (0, 0,
+             _("unparsable value for DIFF_COLORS environment variable"));
+      free (color_buf);
+      for (e = color_ext_list; e != NULL; /* empty */)
+        {
+          e2 = e;
+          e = e->next;
+          free (e2);
+        }
+      colors_enabled = false;
+    }
+}
+
 static void
 check_color_output (bool is_pipe)
 {
@@ -323,6 +705,9 @@ check_color_output (bool is_pipe)
   colors_enabled = (colors_style == ALWAYS
                     || (colors_style == AUTO && output_is_tty));
 
+  if (colors_enabled)
+    parse_diff_color ();
+
   if (output_is_tty)
     install_signal_handlers ();
 }
@@ -923,12 +1308,27 @@ output_1_line (char const *base, char const *limit, char const *flag_format,
     }
 }
 
+enum indicator_no
+  {
+    C_LEFT, C_RIGHT, C_END, C_RESET, C_HEADER, C_ADD, C_DELETE, C_LINE
+  };
+
+static void
+put_indicator (const struct bin_str *ind)
+{
+  fwrite (ind->string, ind->len, 1, outfile);
+}
+
 void
 set_header_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[1m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_HEADER]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
@@ -936,7 +1336,11 @@ set_line_numbers_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[36m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_LINE]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
@@ -944,7 +1348,11 @@ set_add_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[32m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_ADD]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
@@ -952,17 +1360,22 @@ set_delete_color_context (void)
 {
   process_signals ();
   if (colors_enabled)
-    fputs ("\x1B[31m", outfile);
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_DELETE]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 void
 reset_color_context (void)
 {
-  static char const reset_sequence[] = "\x1b[0m";
-  if (! colors_enabled)
-    return;
-
-  fputs (reset_sequence, outfile);
+  if (colors_enabled)
+    {
+      put_indicator (&color_indicator[C_LEFT]);
+      put_indicator (&color_indicator[C_RESET]);
+      put_indicator (&color_indicator[C_RIGHT]);
+    }
 }
 
 char const change_letter[] = { 0, 'd', 'a', 'c' };
-- 
2.4.3