#!/usr/bin/perl -w ######################################################################## # # # ColorDiff - a wrapper/replacment for 'diff' producing # # colourful output # # # # Copyright (C)2002-2012 Dave Ewart (davee@sungate.co.uk) # # # ######################################################################## # # # 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 2 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. # # # ######################################################################## use strict; use Getopt::Long qw(:config pass_through no_auto_abbrev); use IPC::Open2; my $app_name = 'colordiff'; my $version = '1.0.13'; my $author = 'Dave Ewart'; my $author_email = 'davee@sungate.co.uk'; my $app_www = 'http://www.colordiff.org/'; my $copyright = '(C)2002-2012'; my $show_banner = 1; my $color_patch = 0; my $diff_cmd = "diff"; # ANSI sequences for colours my %colour; $colour{white} = "\033[1;37m"; $colour{yellow} = "\033[1;33m"; $colour{green} = "\033[1;32m"; $colour{blue} = "\033[1;34m"; $colour{cyan} = "\033[1;36m"; $colour{red} = "\033[1;31m"; $colour{magenta} = "\033[1;35m"; $colour{black} = "\033[1;30m"; $colour{darkwhite} = "\033[0;37m"; $colour{darkyellow} = "\033[0;33m"; $colour{darkgreen} = "\033[0;32m"; $colour{darkblue} = "\033[0;34m"; $colour{darkcyan} = "\033[0;36m"; $colour{darkred} = "\033[0;31m"; $colour{darkmagenta} = "\033[0;35m"; $colour{darkblack} = "\033[0;30m"; $colour{off} = "\033[0;0m"; # Default colours if /etc/colordiffrc or ~/.colordiffrc do not exist my $plain_text = $colour{white}; my $file_old = $colour{red}; my $file_new = $colour{blue}; my $diff_stuff = $colour{magenta}; my $cvs_stuff = $colour{green}; # Locations for personal and system-wide colour configurations my $HOME = $ENV{HOME}; my $etcdir = '/etc'; my ($setting, $value); my @config_files = ("$etcdir/colordiffrc"); push (@config_files, "$ENV{HOME}/.colordiffrc") if (defined $ENV{HOME}); my $config_file; my $diff_type = 'unknown'; # Convert tabs to spaces sub expand_tabs_to_spaces ($) { my ($s) = @_; while ((my $i = index ($s, "\t")) > -1) { substr ( $s, $i, 1, # range to replace (' ' x (8 - ($i % 8))), # string to replace with ); } $s; } sub check_for_file_arguments { my $nonopts = 0; my $ddash = 0; while (my $arg = shift) { if ($arg eq "--") { $ddash = 1; next; } if ($ddash) { $nonopts++; next; } if ($arg !~ /^-/) { $nonopts++; } if ($arg eq "--help" || $arg eq "--version" || $arg eq "-v") { $nonopts++; } } return $nonopts; } sub detect_diff_type { # Two parameters: # $isref is reference to @inputstream # $allow_diffy is flag indicating whether diffy is a # permitted diff type my $isref = shift; my $allow_diffy = shift; my @is = @$isref; # This may not be perfect - should identify most reasonably # formatted diffs and patches foreach my $record (@is) { # Unified diffs are the only flavour having '+++ ' or '--- ' # at the start of a line if ($record =~ /^(\+\+\+ |--- |@@ )/) { return 'diffu'; } # Context diffs are the only flavour having '***' # at the start of a line elsif ($record =~ /^\*\*\*/) { return 'diffc'; } # Plain diffs have NcN, NdN and NaN etc. elsif ($record =~ /^[0-9,]+[acd][0-9,]+$/) { return 'diff'; } # FIXME - This is not very specific, since the regex matches could # easily match non-diff output. # However, given that we have not yet matched any of the *other* diff # types, this might be good enough # # Only pick diffy if our flag parameter indicates so elsif ( ($allow_diffy == 1) && ($record =~ /(\s\|\s|\s<$|\s>\s)/) ) { return 'diffy'; } # wdiff deleted/added patterns # should almost always be pairwise? elsif ($record =~ /\[-.*?-\]/s || $record =~ /\{\+.*?\+\}/s) { return 'wdiff'; } # FIXME - This is a bit risky, but if we haven't matched any other # diff type by this stage, this line usually indicates we have # debdiff output elsif ($record =~ /^Control files: lines which differ/) { return 'debdiff'; } } $diff_type = 'unknown'; } my $enable_verifymode; my $specified_difftype; GetOptions( # --enable-verifymode option is for testing behaviour of colordiff # against standard test diffs "verifymode" => \$enable_verifymode, "difftype=s" => \$specified_difftype # TODO - check that specified type is valid, issue warning if not ); if (defined $enable_verifymode) { # When in verify mode, to ensure consistent output we don't source # any external config files, so we use built-in defaults # and we add colour to our patches @config_files = (); $color_patch = 1; $show_banner = 0; } foreach $config_file (@config_files) { if (open (COLORDIFFRC, "<$config_file")) { while () { my $colourval; chop; next if (/^#/ || /^$/); s/\s+//g; ($setting, $value) = split ('='); if (!defined $value) { print STDERR "Invalid configuration line ($_) in $config_file\n"; next; } if ($setting eq 'banner') { if ($value eq 'no') { $show_banner = 0; } next; } if ($setting eq 'color_patches') { if ($value eq 'yes') { $color_patch = 1; } next; } if ($setting eq 'diff_cmd') { $diff_cmd = $value; next; } $setting =~ tr/A-Z/a-z/; $value =~ tr/A-Z/a-z/; if (($value eq 'normal') || ($value eq 'none')) { $value = 'off'; } if ($value =~ m/[0-9]+/ && $value >= 0 && $value <= 255) { # Numeric color if( $value < 8 ) { $colourval = "\033[0;3${value}m"; } elsif( $value < 15 ) { $colourval = "\033[0;9${value}m"; } else { $colourval = "\033[0;38;5;${value}m"; } } elsif (defined($colour{$value})) { $colourval = $colour{$value}; } else { print STDERR "Invalid colour specification for setting $setting ($value) in $config_file\n"; next; } if ($setting eq 'plain') { $plain_text = $colourval; } elsif ($setting eq 'oldtext') { $file_old = $colourval; } elsif ($setting eq 'newtext') { $file_new = $colourval; } elsif ($setting eq 'diffstuff') { $diff_stuff = $colourval; } elsif ($setting eq 'cvsstuff') { $cvs_stuff = $colourval; } else { print STDERR "Unknown option in $config_file: $setting\n"; } } close COLORDIFFRC; } } # If output is to a file, switch off colours, unless 'color_patch' is set # Relates to http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=378563 if ((-f STDOUT) && ($color_patch == 0)) { $plain_text = ''; $file_old = ''; $file_new = ''; $diff_stuff = ''; $cvs_stuff = ''; $plain_text = ''; $colour{off} = ''; } # ---------------------------------------------------------------------------- if ($show_banner == 1) { print STDERR "$app_name $version ($app_www)\n"; print STDERR "$copyright $author, $author_email\n\n"; } my $operating_methodology; if (check_for_file_arguments (@ARGV)) { $operating_methodology = 1; # we have files as arg, so we act as diff } else { $operating_methodology = 2; # no files as args, so operate as filter } my @inputstream; my $exitcode = 0; if ($operating_methodology == 1) { # Run diff and then post-process the output my $pid = open2(\*INPUTSTREAM, undef, "$diff_cmd", @ARGV); @inputstream = ; close INPUTSTREAM; waitpid $pid, 0; $exitcode=$? >> 8; } else { # No need to call diff, just process standard input @inputstream = ; } # Input stream has been read - need to examine it # to determine type of diff we have. my $record; my $longest_record = 0; if (defined $specified_difftype) { $diff_type = $specified_difftype; } else { # Detect diff type, diffy is permitted $diff_type = detect_diff_type(\@inputstream, 1); } my $inside_file_old = 1; # ------------------------------------------------------------------------------ # Special pre-processing for side-by-side diffs # Figure out location of central markers: these will be a consecutive set of # three columns where the first and third always consist of spaces and the # second consists only of spaces, '<', '>' and '|' # This is not a 100% certain match, but should be good enough my %separator_col = (); my %candidate_col = (); my $diffy_sep_col = 0; my $mostlikely_sum = 0; if ($diff_type eq 'diffy') { # Not very elegant, but does the job # Unfortunately requires parsing the input stream multiple times foreach (@inputstream) { $record = expand_tabs_to_spaces $_; $longest_record = length ($record) if (length ($record) > $longest_record); } for (my $i = 0 ; $i <= $longest_record ; $i++) { $separator_col{$i} = 1; $candidate_col{$i} = 0; } foreach (@inputstream) { $_ = expand_tabs_to_spaces $_; for (my $i = 0 ; $i < (length ($_) - 2) ; $i++) { next if (!defined $separator_col{$i}); next if ($separator_col{$i} == 0); my $subsub = substr ($_, $i, 2); if ($subsub !~ / [ |<>]/) { $separator_col{$i} = 0; } if ($subsub =~ / [|<>]/) { $candidate_col{$i}++; } } } for (my $i = 0 ; $i < $longest_record - 2 ; $i++) { if ($separator_col{$i} == 1) { if ($candidate_col{$i} > $mostlikely_sum) { $diffy_sep_col = $i; $mostlikely_sum = $i; } } } # If we don't find a suitable separator column then # we've probably misidentified the input as diffy # Search stream again, this time excluding diffy # as a possible outcome if ($diffy_sep_col == 0) { # Detect diff type, diffy is NOT permitted $diff_type = detect_diff_type(\@inputstream, 0); } } # ------------------------------------------------------------------------------ foreach (@inputstream) { if ($diff_type eq 'diff') { if (/^/) { print "$file_new"; } elsif (/^[0-9]/) { print "$diff_stuff"; } elsif (/^(Index: |={4,}|RCS file: |retrieving |diff )/) { print "$cvs_stuff"; } elsif (/^Only in/) { print "$diff_stuff"; } else { print "$plain_text"; } } elsif ($diff_type eq 'diffc') { if (/^- /) { print "$file_old"; } elsif (/^\+ /) { print "$file_new"; } elsif (/^\*{4,}/) { print "$diff_stuff"; } elsif (/^Only in/) { print "$diff_stuff"; } elsif (/^\*\*\* [0-9]+,[0-9]+/) { print "$diff_stuff"; $inside_file_old = 1; } elsif (/^\*\*\* /) { print "$file_old"; } elsif (/^--- [0-9]+,[0-9]+/) { print "$diff_stuff"; $inside_file_old = 0; } elsif (/^--- /) { print "$file_new"; } elsif (/^!/) { if ($inside_file_old == 1) { print "$file_old"; } else { print "$file_new"; } } elsif (/^(Index: |={4,}|RCS file: |retrieving |diff )/) { print "$cvs_stuff"; } else { print "$plain_text"; } } elsif ($diff_type eq 'diffu') { if (/^-/) { print "$file_old"; } elsif (/^\+/) { print "$file_new"; } elsif (/^\@/) { print "$diff_stuff"; } elsif (/^Only in/) { print "$diff_stuff"; } elsif (/^(Index: |={4,}|RCS file: |retrieving |diff )/) { print "$cvs_stuff"; } else { print "$plain_text"; } } # Works with previously-identified column containing the diff-y # separator characters elsif ($diff_type eq 'diffy') { if (length ($_) > ($diffy_sep_col + 2)) { my $sepchars = substr ($_, $diffy_sep_col, 2); if ($sepchars eq ' <') { print "$file_old"; } elsif ($sepchars eq ' |') { print "$diff_stuff"; } elsif ($sepchars eq ' >') { print "$file_new"; } else { print "$plain_text"; } } elsif (/^Only in/) { print "$diff_stuff"; } else { print "$plain_text"; } } elsif ($diff_type eq 'wdiff') { $_ =~ s/(\[-.+?-\])/$file_old$1$colour{off}/g; $_ =~ s/(\{\+.+?\+\})/$file_new$1$colour{off}/g; } elsif ($diff_type eq 'debdiff') { $_ =~ s/(\[-.+?-\])/$file_old$1$colour{off}/g; $_ =~ s/(\{\+.+?\+\})/$file_new$1$colour{off}/g; } s/$/$colour{off}/; print "$_"; } exit $exitcode;