#!/usr/bin/env perl # tdiff - diff for terminals (colorized, with UTF-8 conversion) # Copyright 2006-2010 Vincent Lefevre . # 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. # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA # Documentation and history are provided at the end of the file. use strict; my ($proc) = '$Id: tdiff 37358 2010-06-17 07:59:38Z vinc17/ypig $' =~ /^.Id: (\S+) / or die; my @color = qw/setaf setab/; my %tput; # Use 'our' to allow the config files to see and modify these variables. our $eight_bit_encoding = 'iso-8859-1'; our $end = &tput('sgr0'); our %start = ('normal' => '', 'old' => &tput('bold:1'), 'new' => &tput('bold:2'), 'same' => &tput('bold:7'), 'hunk' => &tput('bold:5'), 'info' => &tput('bold:4'), 'nonl' => &tput('bold:5'), 'dir' => &tput('bold:4'), 'cvssvn' => &tput('bold:3'), ); # Assume no UTF-8 by default. our $utf8 = eval { require I18N::Langinfo; I18N::Langinfo::langinfo(I18N::Langinfo::CODESET()) eq 'UTF-8'; } or eval { require POSIX; POSIX::setlocale('LC_CTYPE') =~ /utf-?8/i; }; foreach ($ENV{'TDIFFRC'} ? split /:/, $ENV{'TDIFFRC'} : ("/etc/tdiffrc", "$ENV{HOME}/.tdiffrc")) { # For security reasons, when tdiff is run as root, the config file # must be owned by root to be evaluated. do $_ if $< || -o $_; } my $stream; # Without arguments, the diff is read from the standard input stream. # Otherwise, the 'diff' command is called with the provided arguments # and tdiff acts as a wrapper, processing the 'diff' output. if (@ARGV) { open DIFF, "-|", 'diff', @ARGV or die "$0: can't exec diff: $!\n"; $stream = *DIFF; } else { $stream = *STDIN; } my $range = qr/(\d+)(?:,(\d+))?/; my ($ctx,$fileconv,$hend,$hm,$hp,$index); while (<$stream>) { defined $ctx or &other, next; if ($ctx eq 'u') { my $s = substr $_, 0, 1; if ($s eq ' ') { &output('same'); $hm--; $hp--; } elsif ($s eq '-') { &output('old'); $hm--; } elsif ($s eq '+') { &output('new'); $hp--; } else { &other; } } elsif ($ctx eq 'c' && defined $hm && substr($_, 0, 3) eq '---') { &output('hunk'); undef $hm; } elsif ($ctx eq 'a' || ($ctx eq 'c' && !defined $hm)) { if (substr($_, 0, 2) eq '> ') { &output('new'); $hp--; } else { &other; } } elsif ($ctx eq 'd' || ($ctx eq 'c' && defined $hm)) { if (substr($_, 0, 2) eq '< ') { &output('old'); $hm--; } else { &other; } } else { die "$0: internal error" } $hend = $. if $hm == 0 && $hp == 0; $hm > 0 || $hp > 0 or undef $ctx; } if (@ARGV) { close DIFF; exit 255 if $? & 127; exit $? >> 8; } sub nr { defined $_[1] ? $_[1] - $_[0] + 1 : 1 } sub other { if (/^(@@ -(?:\d+,)?(\d+) \+(?:\d+,)?(\d+) @@)(.*)/) { ($ctx,$hm,$hp) = ('u',$2,$3); if ($4 eq '') { &output('hunk'); } else { $_ = $1; my $hs = &process('hunk'); $_ = $4; my $hi = &process('info'); print "${hs}${hi}\n"; } return; } /^\d+a${range}$/ and &output('hunk'), ($ctx,$hm,$hp) = ('a',undef,&nr($1,$2)), return; /^${range}d\d+$/ and &output('hunk'), ($ctx,$hm,$hp) = ('d',&nr($1,$2),undef), return; /^${range}c${range}$/ and &output('hunk'), ($ctx,$hm,$hp) = ('c',&nr($1,$2),&nr($3,$4)), return; undef $fileconv; # No longer hunks for current file. my $s4 = substr $_, 0, 4; $s4 eq '--- ' and &output('old'), return; $s4 eq '+++ ' and &output('new'), return; substr($_, 0, 2) eq '\\ ' && $hend + 1 == $. and &output('nonl'), return; /^(diff |Only in )/ and &output('dir'), return; /^Index: / and &output('cvssvn'), $index = $., return; /^(RCS file: |retrieving )/ || (/^={5,}/ && $index + 1 == $.) and &output('cvssvn'), return; &output('normal'); } sub tput { if (!defined $tput{$_[0]}) { if ($_[0] =~ /:/) { my $i = 0; foreach (split /:/, $_[0]) { $tput{$_[0]} .= &tput(/^\d+$/ ? $color[$i++]." $_" : $_) } } else { $tput{$_[0]} = `tput $_[0]`; } } return $tput{$_[0]}; } sub process { chomp; # In a UTF-8 environment, automatically convert data into UTF-8 # when invalid sequences are found. if ($utf8) { eval { require Encode; my $s = $_; Encode::decode('utf8', $s, 1); }; $fileconv = 1 if $@ =~ /^utf8.*does not map to Unicode/; Encode::from_to($_, $eight_bit_encoding, 'utf8') if $fileconv; } return "$start{$_[0]}$_$end"; } sub output { print &process($_[0])."\n" } # Short documentation (more information by reading the source): # * Usage: tdiff [diff arguments] # * Examples: # tdiff -u file1 file2 # diff -u file1 file2 | tdiff # tdiff < patchfile # svn diff | tdiff # cvs diff | tdiff # * You can modify the default configuration by putting commands into # config files /etc/tdiffrc and $HOME/.tdiffrc (or more precisely, # files specified by the TDIFFRC environment variable -- by default, # it is equivalent to "/etc/tdiffrc:$ENV{HOME}/.tdiffrc"). Commands # are just Perl ones. Examples: # $eight_bit_encoding = 'iso-8859-15'; # $start{'hunk'} = &tput('bold:4'); # As these config files are Perl scripts, they must be owned by root # when root executes tdiff. # If your shell is zsh, you can even do: # svn diff | TDIFFRC=<(echo 'undef $utf8') tdiff # History: # 2006-11-10: First version (unified diff format only). # 2006-11-13: Added hunk detection and automatical conversion into UTF-8. # 2007-02-12: Added support for copied context. # 2007-02-14: Added info color (for patches produced with "diff -p"). # 2007-02-17: Added support for config files. # 2007-02-18: Improved conversion into UTF-8. First released version. # 2008-04-15: Improved open with pipe (forgot to test if fork failed). # 2010-06-17: Better UTF-8 charmap detection (with I18N::Langinfo). # The latest version of tdiff can be obtained at the following URL: # http://www.vinc17.net/unix/#tdiff # or # http://www.vinc17.org/unix/#tdiff