#!/usr/bin/env perl
##
## sdif: sdiff clone
##
## Copyright (c) 1992- Kazumasa Utashiro
##
## Original version on Jul 24 1991
##
=pod
=head1 NAME
sdif - side-by-side diff viewer for ANSI terminal
=head1 VERSION
Version 4.40
=head1 SYNOPSIS
sdif file_1 file_2
diff ... | sdif
OPTIONS:
-i, --ignore-case
-b, --ignore-space-change
-w, --ignore-all-space
-B, --ignore-blank-lines
--[no]number, -n print line number
--digit=# set the line number digits (default 4)
--truncate, -t truncate long line
--boundary=# line folding boundary (default word)
--context, -c, -C# context diff
--unified, -u, -U# unified diff
--width=#, -W# width of output (default 80)
--margin=# margin column number (default 0)
--runin=# run-in column number (default --margin)
--runout=# run-out column number (default --margin)
--mark=position mark position (right, left, center, side) or no
--column=order column order (default ONM)
--view, -v viewer mode
--parallel[=#], -V treat unknown text as common part (default 2)
--ambiguous=s ambiguous character width (detect, wide, narrow)
--[no-]command print diff control command (default on)
--[no-]filename print diff filename (default on)
--[no-]prefix process git --graph output (default on)
--prefix-pattern prefix pattern
--color=when 'always' (default), 'never' or 'auto'
--nocolor --color=never
--colormap, --cm specify color map
--colortable[=#] show color table (optional #: 6, 12, 24)
--[no-]256 on/off ANSI 256 color mode (default on)
--[no-]cc color command line (default true)
--[no-]fc file name (default true)
--[no-]lc line number (default true)
--[no-]mc diff mark (default true)
--[no-]tc normal text (default true)
--[no-]uc unknown text (default true)
--man display manual page
--diff=s set diff command
--diffopts=s set diff command options
--[no-]lenience supress unexpected input warning (default on)
--visible xx=1 set visible chars
--tabhead=char set tabhead char
--tabspace=char set tabspace char
--tabstyle=style set tabstyle (dot, symbol, shade, bar, dash...)
--tabstop=# set tabstop width (default 8)
--[no-]cdif use ``cdif'' as word context diff backend
--unit=s pass through to cdif (word, char, mecab)
--cdifopts=s set cdif command options
=cut
use v5.14;
use utf8;
use Encode;
use open IO => ':utf8';
use Carp;
use charnames ':full';
use List::Util qw(min max reduce sum pairmap first);
use Text::ParseWords qw(shellwords);
$Data::Dumper::Terse = 1;
}
my $version = $App::sdif::VERSION;
my $default_cdif = 'cdif';
my @default_cdifopts = qw(--sdif);
my $default_lenience = 1;
my $default_256 = 1;
my $default_prefix = 1;
my $default_prefix_pattern = q/(?:\\| )*(?: )?/;
my @cdifopts;
my $read_stdin;
our $screen_width;
if (my $env = $ENV{'SDIFOPTS'}) {
unshift @ARGV, shellwords($env);
}
use open IO => ':utf8', ':std';
map { $_ = decode 'utf8', $_ unless utf8::is_utf8($_) } @ARGV;
my $app;
Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
has help => ' h ' ;
has man => ' ' ;
has debug => 'd + ' ;
has number => 'n ! ' ;
has digit => ' =i ' , default => 4 ;
has column => ' =s ' ;
has truncate => 't ! ' ;
has boundary => ' =s ' , default => 'word' ;
has onword => ' ! ' ;
has mark => ' =s ' , default => 'center' ;
has prefix => ' ! ' , default => $default_prefix ;
has prefix_pattern => ' =s ' , default => $default_prefix_pattern ;
has width => 'W =i ' ;
has margin => ' =i ' , default => 0 ;
has runin => ' =i ' ;
has runout => ' =i ' ;
has view => 'v ! ' ;
has parallel => 'V :2 ' , default => 0 ;
has ambiguous => ' =s ' , default => 'narrow' ;
has filename => ' ! ' , default => 1 ;
has command => ' ! ' , default => 1 ;
has diff => ' =s ' , default => 'diff' ;
has diffopts => ' =s@' , default => [] ;
has color => ' =s ' , default => 'always' ;
has colormap => 'cm=s@' , default => [] ;
has colordump => ' ' ;
has 256 => ' ! ' , default => $default_256 ;
has commandcolor => 'cc! ' , default => 1 ;
has filecolor => 'fc! ' , default => 1 ;
has linecolor => 'lc! ' , default => 1 ;
has markcolor => 'mc! ' , default => 1 ;
has textcolor => 'tc! ' , default => 1 ;
has unknowncolor => 'uc! ' , default => 1 ;
has cdif => ' :s ' , default => '';
has cdifopts => ' =s ' ;
has colortable => ' :s ' , any => qr/^(|6|12|24)$/;
has lenience => ' ! ' , default => $default_lenience ;
has visible => ' =i%' , default => {} ;
has tabstop => ' =i ' , default => 8;
has tabhead => ' =s ' ;
has tabstyle => 'ts=s ' ;
has tabspace => ' =s ' ;
has unit => 'by:s ' ;
has ignore_case => 'i ' ;
has ignore_space_change => 'b ' ;
has ignore_all_space => 'w ' ;
has ignore_blank_lines => 'B ' ;
has context => 'C =i' ;
has unified => 'U =i' ;
has c => ' ' ;
has u => ' ' ;
has '+onword'
=> sub { $_->boundary = $_[1] ? 'word' : '' } ;
has '+cdifopts'
=> sub { push @cdifopts, shellwords $_[1] } ;
has '+ignore_case'
=> sub { push @{$app->diffopts}, '-i'; push @cdifopts, '-i' } ;
has '+ignore_space_change'
=> sub { push @{$app->diffopts}, '-b'; push @cdifopts, '-w' } ;
has '+ignore_all_space'
=> sub { push @{$app->diffopts}, '-w'; push @cdifopts, '-w' } ;
has '+ignore_blank_lines'
=> sub { push @{$app->diffopts}, '-B' } ;
has '+c' => sub { push @{$app->diffopts}, '-c' } ;
has '+u' => sub { push @{$app->diffopts}, '-u' } ;
has '+context' => sub { push @{$app->diffopts}, '-C' . $_[1] } ;
has '+unified' => sub { push @{$app->diffopts}, '-U' . $_[1] } ;
has nocolor => 'no-color' , action => sub { $app->color = 'never' } ;
has nocdif => 'no-cdif' , action => sub { $app->cdif = undef } ;
has mecab => '!' , action => sub { $app->unit = $_[1] ? 'mecab' : undef } ;
has '+ambiguous' => sub {
if ($_[1] =~ /^(?:wide|full)/) {
$Text::VisualWidth::PP::EastAsian = 1;
Text::ANSI::Fold->configure(ambiguous => 'wide');
}
} ;
has '+help' => sub { usage() } ;
has '+man' => sub { pod2usage {-verbose => 2} } ;
} no Getopt::EX::Hashed;
$app = Getopt::EX::Hashed->new() or die;
my @SAVEDARGV = @ARGV;
use Getopt::EX::Long qw(:DEFAULT Configure ExConfigure);
ExConfigure BASECLASS => [ "App::sdif", "Getopt::EX" ];
Configure "bundling";
$app->getopt or usage({status => 1});
warn "\@ARGV = (@SAVEDARGV)\n" if $app->debug;
$App::sdif::Util::NO_WARNINGS = $app->lenience;
use Text::VisualWidth::PP qw(vwidth);
use Text::ANSI::Fold qw(ansi_fold :constants); {
Text::ANSI::Fold->configure(padding => 1,
expand => 1,
tabstop => $app->tabstop);
}
$app->visible->{ht} //= 1 if $app->tabstyle;
if ($app->visible->{ht}) {
Text::ANSI::Fold->configure(
tabstyle => $app->tabstyle,
map { $_->[0] => unicode($_->[1]) }
grep { $_->[1] }
[ tabhead => $app->tabhead ],
[ tabspace => $app->tabspace ],
);
}
sub unicode {
my $char = shift or return undef;
if ($char =~ /^\X$/) {
$char;
} else {
eval qq["\\N{$char}"] or die "$!";
}
}
if ($app->margin > 0) {
Text::ANSI::Fold->configure(
linebreak => LINEBREAK_ALL,
margin => $app->margin,
runin => $app->runin // $app->margin,
runout => $app->runout // $app->margin,
);
}
my %colormap = do {
my $col = $app->{256} ? 0 : 1;
pairmap { $a => (ref $b eq 'ARRAY') ? $b->[$col] : $b } (
UNKNOWN => "" ,
OCOMMAND => [ "555/010" , "GS" ],
NCOMMAND => [ "555/010" , "GS" ],
MCOMMAND => [ "555/010" , "GS" ],
OFILE => [ "551/010D" , "GDS" ],
NFILE => [ "551/010D" , "GDS" ],
MFILE => [ "551/010D" , "GDS" ],
OMARK => [ "010/444" , "G/W" ],
NMARK => [ "010/444" , "G/W" ],
MMARK => [ "010/444" , "G/W" ],
UMARK => "" ,
OLINE => [ "220" , "Y" ],
NLINE => [ "220" , "Y" ],
MLINE => [ "220" , "Y" ],
ULINE => "" ,
OTEXT => [ "K/454" , "G" ],
NTEXT => [ "K/454" , "G" ],
MTEXT => [ "K/454" , "G" ],
UTEXT => "" ,
);
};
$Getopt::EX::Colormap::NO_RESET_EL = 1;
use constant SGR_RESET => "\e[m";
my $color_handler = Getopt::EX::Colormap
->new(HASH => \%colormap)
->load_params(@{$app->colormap});
$colormap{OUMARK} ||= $colormap{UMARK} || $colormap{OMARK};
$colormap{NUMARK} ||= $colormap{UMARK} || $colormap{NMARK};
$colormap{OULINE} ||= $colormap{ULINE} || $colormap{OLINE};
$colormap{NULINE} ||= $colormap{ULINE} || $colormap{NLINE};
for (
[ $app->unknowncolor => q/UNKNOWN/ ],
[ $app->commandcolor => q/COMMAND/ ],
[ $app->filecolor => q/FILE/ ],
[ $app->linecolor => q/LINE/ ],
[ $app->markcolor => q/MARK/ ],
[ $app->textcolor => q/TEXT/ ],
) {
my($color, $label) = @$_;
$color and next;
for (grep /$label/, keys %colormap) {
$colormap{$_} = '';
}
}
if ($app->colordump) {
print $color_handler->colormap(
name => '--changeme', option => '--colormap');
exit;
}
my $painter = do {
if (($app->color eq 'always')
or (($app->color eq 'auto') and (-t STDOUT))) {
sub { $color_handler->color(@_) };
} else {
sub { $_[1] } ;
}
};
##
## setup cdif command and option
##
if (defined $app->cdif and $app->cdif eq '') {
$app->cdif = $default_cdif;
}
for (
[ "unit" , "=" , $app->unit , undef ] ,
[ "256" , "!" , $app->{256} , $default_256 ] ,
[ "prefix" , "!" , $app->prefix , $default_prefix ] ,
[ "lenience" , "!" , $app->lenience , $default_lenience ] ,
)
{
my($name, $type, $var, $default) = @$_;
if ($type eq "!") {
next if not defined $var;
next if $var == $default;
unshift @cdifopts, sprintf("--%s%s", $var ? '' : 'no-', $name);
} elsif ($type eq "=") {
next if not defined $var;
unshift @cdifopts, sprintf("--%s=%s", $name, $var);
} else {
die;
}
}
unshift @cdifopts, @default_cdifopts;
my($OLD, $NEW, $DIFF);
if (@ARGV == 2) {
($OLD, $NEW) = @ARGV;
$DIFF = "$app->{diff} @{$app->{diffopts}} $OLD $NEW |";
} elsif (@ARGV < 2) {
$DIFF = shift || '-';
$read_stdin++;
} else {
usage({status => 1}, "Unexpected arguments.\n\n");
}
my $readfile =
($OLD and $NEW) && !$read_stdin && !(grep { /^-[cuCU]/ } @{$app->diffopts});
use constant {
RIGHT => 'right',
LEFT => 'left',
NO => 'no',
};
my %markpos = (
center => [ RIGHT , LEFT , LEFT ],
side => [ LEFT , RIGHT , LEFT ],
right => [ RIGHT , RIGHT , RIGHT ],
left => [ LEFT , LEFT , LEFT ],
no => [ NO , NO , NO ],
none => [ NO , NO , NO ],
);
unless ($markpos{$app->mark}) {
my @keys = sort keys %markpos;
usage "Use one from (@keys) for option --mark\n\n";
}
my @markpos = @{$markpos{$app->mark}};
my($omarkpos, $nmarkpos, $mmarkpos) = @markpos;
my $num_format = sprintf '%%%dd', $app->digit;
$screen_width = $app->width || &terminal_width;
sub column_width {
my $column = shift;
state %column_width;
$column_width{$screen_width * 1000 + $column} //= do {
use integer;
my $w = $screen_width;
$w -= $column if $app->mark;
max 1, $w / $column;
};
}
##
## --colortable
##
if (defined(my $n = $app->colortable)) {
no strict 'refs';
&{"Getopt::EX::Colormap::colortable$n"}($screen_width);
exit;
}
##
## Column order
##
my @column = !$app->column ? () : do {
map { $_ - 1 }
map { { O=>1, N=>2, M=>3 }->{$_} // $_ }
$app->column =~ /[0-9ONM]/g;
};
##
## Git --graph prefix pattern
##
my $prefix_re = do {
if ($app->prefix) {
qr/$app->{prefix_pattern}/;
} else {
"";
}
};
if ($app->debug) {
printf STDERR "\$OLD = %s\n", $OLD // "undef";
printf STDERR "\$NEW = %s\n", $NEW // "undef";
printf STDERR "\$DIFF = %s\n", $DIFF // "undef";
}
if ($app->cdif) {
my $pid = open DIFF, '-|';
if (not defined $pid) {
die "$!" if not defined $pid;
}
## child
elsif ($pid == 0) {
if ($DIFF ne '-') {
open(STDIN, $DIFF) || die "cannot open diff: $!\n";
}
do { exec shellwords($app->cdif), @cdifopts } ;
warn "exec failed: $!";
print while <>;
exit;
}
## parent
else {
## nothing to do
}
} else {
open(DIFF, $DIFF) || die "cannot open diff: $!\n";
}
if ($readfile) {
binmode DIFF, ':raw';
my $DIFFOUT = do { local $/; <DIFF> };
close DIFF;
open DIFF, '<', \$DIFFOUT or die;
open OLD, $OLD or die "$OLD: $!\n";
open NEW, $NEW or die "$NEW: $!\n";
# For reading /dev/fd/*
seek OLD, 0, 0 or die unless -p OLD;
seek NEW, 0, 0 or die unless -p NEW;
}
my @boundary = (boundary => $app->boundary);
my $color_re = qr{ \e \[ [\d;]* [mK] }x;
my $oline = 1;
my $nline = 1;
my $mline = 1;
while (<DIFF>) {
#
# normal diff
#
if (/^([\d,]+)([adc])([\d,]+)$/) {
my(@old, @new);
my($left, $ctrl, $right) = ($1, $2, $3);
my($l1, $l2) = range($left);
my($r1, $r2) = range($right);
if ($readfile) {
my $identical_line = $l1 - $oline + 1 - ($ctrl ne 'a');
print_identical($identical_line);
}
if ($app->debug || $read_stdin) {
print_command_n($_, $_);
}
if ($ctrl eq 'd' || $ctrl eq 'c') {
($oline) = $left =~ /^(\d+)/;
my $n = $l2 - $l1 + 1;
@old = read_line(*DIFF, $n);
$readfile and read_line(*OLD, $n);
}
read_line(*DIFF, 1) if $ctrl eq 'c';
if ($ctrl eq 'a' || $ctrl eq 'c') {
($nline) = $right =~ /^(\d+)/;
my $n = $r2 - $r1 + 1;
@new = read_line(*DIFF, $n);
$readfile and read_line(*NEW, $n);
}
map {
s/^([<>])\s?/{'<' => '-', '>' => '+'}->{$1}/e
} @old, @new;
flush_buffer([], \@old, \@new);
}
#
# context diff
#
elsif (/^\*\*\* /) {
my $next = <DIFF>;
print_command_n({ type => 'FILE' }, $_, $next);
}
elsif ($_ eq "***************\n") {
my(@old, @new);
my $ohead = $_ = <DIFF>;
my($left, $right);
unless (($left) = /^\*\*\* ([\d,]+) \*\*\*\*$/) {
print;
next;
}
my $oline = range($left);
my $dline = 0;
my $cline = 0;
my $nhead = $_ = <DIFF>;
unless (($right) = /^--- ([\d,]+) ----$/) {
@old = read_line(*DIFF, $oline - 1, $nhead);
$nhead = $_ = <DIFF>;
unless (($right) = /^--- ([\d,]+) ----$/) {
print $ohead, @old, $_;
next;
}
for (@old) {
/^-/ and ++$dline;
/^!/ and ++$cline;
}
}
my $nline = range($right);
if (@old == 0 or $cline != 0 or ($oline - $dline != $nline)) {
@new = read_line(*DIFF, $nline);
}
print_command_n($ohead, $nhead);
($oline) = $left =~ /^(\d+)/;
($nline) = $right =~ /^(\d+)/;
my @buf = merge_diffc(\@old, \@new);
flush_buffer(@buf);
}
#
# unified diff
#
elsif (/^($prefix_re)(--- (?s:.*))/) {
my($prefix, $left) = ($1, $2);
my $right = <DIFF>;
local $screen_width = $screen_width;
if ($prefix) {
$right =~ s/^\Q$prefix//;
print $prefix;
$screen_width -= length $prefix;
}
print_command_n({ type => 'FILE' }, $left, $right);
}
elsif (m{^
(?<prefix>$prefix_re)
(?<command>
\@\@ [ ]
\-(?<oline>\d+) (?:,(?<o>\d+))? [ ]
\+(?<nline>\d+) (?:,(?<n>\d+))? [ ]
\@\@
(?s:.*)
)
}x) {
($oline, $nline) = @+{qw(oline nline)};
my($o, $n) = ($+{o}//1, $+{n}//1);
my($prefix, $command) = @+{qw(prefix command)};
local $screen_width = $screen_width;
my($divert, %read_opt);
if ($prefix) {
$screen_width -= length $prefix;
$read_opt{prefix} = $prefix;
$divert = App::sdif::Divert->new(FINAL => sub { s/^/$prefix/mg });
}
print_command_n({ type => 'COMMAND' }, $command, $command);
my @buf = read_unified_2 \%read_opt, *DIFF, $o, $n;
flush_buffer(@buf);
}
#
# diff --combined (only support 3 files)
#
elsif (/^diff --(?:cc|combined)/) {
my @lines = ($_);
push @lines, read_until { /^\+\+\+/ } *DIFF;
if (not defined $lines[-1]) {
pop @lines;
print @lines;
next;
}
print @lines;
}
elsif (/^\@{3} -(\d+)(?:,(\d+))? -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? \@{3}/) {
print_command_n({ type => 'COMMAND' }, $_, $_, $_);
($oline, $nline, $mline) = ($1, $3, $5);
state $read_unified_3 = read_unified_sub(3);
my @buf = $read_unified_3->(*DIFF, $2 // 1, $4 // 1, $6 // 1);
flush_buffer_3(@buf);
}
#
# conflict marker
#
elsif (/^<<<<<<<\h*+(.*)/) {
CONFLICT:
{
my %name = (o => $_);
my($c1, $c2, $c3, $c4);
$c1 = $_;
my @old = read_until { /^=======$/ } *DIFF;
$c2 = pop @old // do {
flush_unknown($c1, @old);
last;
};
my @new = read_until { /^>>>>>>>\h*+(.*)/ } *DIFF;
$c4 = pop @new // do {
flush_unknown($c1, @old, $c2, @new);
last;
};
$name{n} = $c4;
my @mrg;
my $mrg = first { $old[$_] =~ /^\Q|||||||\E\h*+(.*)/ } keys @old;
if (defined $mrg or $app->parallel > 2) {
if (defined $mrg) {
$name{m} = $old[$mrg];
($c3, @mrg) = splice @old, $mrg;
} else {
$name{m} = $name{o};
$name{o} = $name{n};
@mrg = @old;
@old = @new;
}
s/^/--/ for @old, @mrg;
s/^/++/ for @new;
print_command_n({ type => 'FILE' }, @name{qw(o m n)});
flush_buffer_3([], \@old, \@mrg, \@new);
} else {
s/^/-/ for @old;
s/^/+/ for @new;
print_command_n({ type => 'FILE' }, @name{qw(o n)});
flush_buffer([], \@old, \@new);
}
}
}
#
# #ifdef custom container
#
# #ifdef JA ::::::: ja
# japanese text japanese text
# #endif :::::::
# #ifdef EN ::::::: en
# english text english text
# #endif :::::::
#
elsif (/^(\#ifdef|:{7,})\h++(.*)/) {
COLON:
{
my($c1, $c2, $c3, $c4) = ($_);
my $start = $1;
my $end = $start eq '#ifdef' ? qr/^#endif$/ : qr/^$start$/;
my($m1, $m2) = ($2);
my @old = read_until { /$end/ } *DIFF;
$c2 = pop @old // do {
flush_unknown($c1, @old);
last;
};
$c3 = <DIFF>;
if ($c3 !~ /^${start}\h++(.*)/) {
flush_unknown($c1, @old, $c2, $c3);
last;
}
$m2 = $1;
my @new = read_until { /$end/ } *DIFF;
$c4 = pop @new // do {
flush_unknown($c1, @old, $c2, $c3, @new);
last;
};
@old = (" $c1", map(s/^/-/r, @old), " $c2");
@new = (" $c3", map(s/^/+/r, @new), " $c4");
flush_buffer([], \@old, \@new);
}
}
else {
flush_unknown($_);
}
}
continue {
STDOUT->flush;
}
close DIFF;
my $exit = $DIFF =~ /\|$/ ? $? >> 8 : 0;
if ($readfile) {
if ($exit < 2) {
print_identical(-1);
}
close OLD;
close NEW;
}
exit($exit > 1);
######################################################################
##
## Convert diff -c output to -u compatible format.
##
sub merge_diffc {
my @o = @{+shift};
my @n = @{+shift};
for (@o, @n) {
s/(?<= ^[ \-\+\!] ) [\t ]//x or die "Format error (-c).\n";
}
my @buf;
while (@o or @n) {
push @buf, \( my( @common, @old, @new ) );
while (@o and $o[0] =~ /^ /) {
push @common, shift @o;
shift @n if @n;
}
while (@n and $n[0] =~ /^ /) {
push @common, shift @n;
}
push @old, shift @o while @o and $o[0] =~ /^\-/;
next if @old;
push @new, shift @n while @n and $n[0] =~ /^\+/;
next if @new;
push @old, shift @o while @o and $o[0] =~ s/^!/-/;
push @new, shift @n while @n and $n[0] =~ s/^!/+/;
}
@buf;
}
sub flush_unknown {
if ($app->parallel > 2) {
flush_buffer_3( [ map s/^/ /r, @_ ] );
}
elsif ($app->parallel > 1) {
flush_buffer( [ map s/^/ /r, @_ ] );
}
else {
print $painter->('UNKNOWN', $_) for @_;
}
}
sub flush_buffer {
push @_, [] while @_ % 3;
if ($app->view) {
@_ = do {
map { @$_ }
reduce {
[ [] ,
[ map { @$_ } $a->[1], $b->[0], $b->[1] ] ,
[ map { @$_ } $a->[2], $b->[0], $b->[2] ] ] }
map { $_ ? [ ( splice @_, 0, 3 ) ] : [ [], [], [] ] }
0 .. @_ / 3 ;
};
}
while (my($s, $o, $n) = splice @_, 0, 3) {
for (@$s) {
s/^(.)// or die;
print_column_23($1, $_, $1, $_);
$oline++;
$nline++;
}
while (@$o or @$n) {
my $old = shift @$o;
my $new = shift @$n;
my $omark = $old ? $old =~ s/^(.)// && $1 : ' ';
my $nmark = $new ? $new =~ s/^(.)// && $1 : ' ';
print_column_23($omark, $old, $nmark, $new);
$oline++ if defined $old;
$nline++ if defined $new;
}
}
}
sub flush_buffer_3 {
push @_, [] while @_ % 4;
if ($app->view) {
@_ = do {
map { @$_ }
reduce {
[ [] ,
[ map { @$_ } $a->[1], $b->[0], $b->[1] ] ,
[ map { @$_ } $a->[2], $b->[0], $b->[2] ] ,
[ map { @$_ } $a->[3], $b->[0], $b->[3] ] ] }
map { $_ ? [ splice @_, 0, 4 ] : [ [], [], [], [] ] }
0 .. @_ / 4;
};
}
while (@_) {
my @d = splice @_, 0, 4;
for my $common (@{shift @d}) {
$common =~ s/^ //;
print_column_23(' ', $common, ' ', $common, ' ', $common);
$oline++;
$nline++;
$mline++;
}
while (first { @$_ > 0 } @d) {
my $old = shift @{$d[0]};
my $new = shift @{$d[1]};
my $mrg = shift @{$d[2]};
my $om = $old ? $old =~ s/^(?|(\-).| (\+)|( ) )// && $1 : ' ';
my $nm = $new ? $new =~ s/^(?|.(\-)|(\+) |( ) )// && $1 : ' ';
my $mm = $mrg ? $mrg =~ s/^(?|(\+).|.(\+)|( ) )// && $1 : ' ';
print_column_23($om, $old, $nm, $new, $mm, $mrg);
$oline++ if defined $old;
$nline++ if defined $new;
$mline++ if defined $mrg;
}
}
}
sub print_identical {
my $n = shift;
while ($n--) {
my $old = <OLD>;
my $new = <NEW>;
defined $old or defined $new or last;
print_column_23(' ', $old, ' ', $new);
$oline++;
$nline++;
$mline++;
}
}
sub linenum {
my $n = shift;
defined $n ? (sprintf $num_format, $n) : (' ' x $app->digit);
}
sub print_column_23 {
my $column = @_ / 2;
my $width = column_width $column;
my($omark, $old, $nmark, $new, $mmark, $mrg) = @_;
my $print_number = $app->number;
my($onum, $nnum, $mnum) = ('', '', '');
my $nspace = $print_number ? ' ' : '';
if (defined $old) {
chomp $old;
$onum = linenum($oline) if $print_number;
}
if (defined $new) {
chomp $new;
$nnum = linenum($nline) if $print_number;
}
if (defined $mrg) {
chomp $mrg;
$mnum = linenum($mline) if $print_number;
}
my($OTEXT, $OLINE, $OMARK) =
$omark =~ /\S/ ? qw(OTEXT OLINE OMARK) : qw(UTEXT OULINE OUMARK);
my($NTEXT, $NLINE, $NMARK) =
$nmark =~ /\S/ ? qw(NTEXT NLINE NMARK) : qw(UTEXT NULINE NUMARK);
my($MTEXT, $MLINE, $MMARK) =
$mmark =~ /\S/ ? qw(MTEXT MLINE MMARK) : qw(UTEXT NULINE NUMARK)
if $column >= 3;
while (1) {
(my $o, $old) = ansi_fold($old,
max(1, $width - length($onum . $nspace)),
@boundary);
(my $n, $new) = ansi_fold($new,
max(1, $width - length($nnum . $nspace)),
@boundary);
(my $m, $mrg) = ansi_fold($mrg,
max(1, $width - length($mnum . $nspace)),
@boundary)
if $column >= 3;
my @f;
$f[0]{MARK} = $painter->($OMARK, $omark);
$f[0]{LINE} = $painter->($OLINE, $onum) . $nspace if $print_number;
$f[0]{TEXT} = $painter->($OTEXT, $o) if $o ne "";
$f[1]{MARK} = $painter->($NMARK, $nmark);
$f[1]{LINE} = $painter->($NLINE, $nnum) . $nspace if $print_number;
$f[1]{TEXT} = $painter->($NTEXT, $n) if $n ne "";
if ($column >= 3) {
$f[2]{MARK} = $painter->($MMARK, $mmark);
$f[2]{LINE} = $painter->($MLINE, $mnum) . $nspace if $print_number;
$f[2]{TEXT} = $painter->($MTEXT, $m) if $m ne "";
}
print_field_n(@f);
last if $app->truncate;
last unless $old ne '' or $new ne '' or ($mrg and $mrg ne '');
if ($print_number) {
$onum =~ s/./ /g;
$nnum =~ s/./ /g;
$mnum =~ s/./ /g if $column >= 3;
}
$omark = $old ne '' ? '.' : ' ';
$nmark = $new ne '' ? '.' : ' ';
$mmark = $mrg ne '' ? '.' : ' ' if $column >= 3;
}
}
sub print_command_n {
my $opt = ref $_[0] ? shift : {};
my $column = @_;
my $width = column_width $column;
my @f;
$opt->{type} //= 'COMMAND';
$app->command or return if $opt->{type} eq 'COMMAND';
$app->filename or return if $opt->{type} eq 'FILE';
my @color = map { $_ . $opt->{type} } "O", "N", "M";
for my $i (keys @_) {
local $_ = $_[$i];
chomp if defined;
($_) = ansi_fold($_, $width);
my %f;
my $color = $i < @color ? $color[$i] : $color[-1];
$f{TEXT} = $painter->($color, $_);
$f{MARK} = ' ';
push @f, \%f;
}
print_field_n(@f);
}
sub print_field_n {
if (@column >= @_) {
@_ = @_[ @column[ keys @_ ] ];
}
while (my($i, $f) = each @_) {
my $markpos = $i < @markpos ? $markpos[$i] : $markpos[-1];
local $_;
$_ = $f->{"MARK"} and print if $markpos eq LEFT;
$_ = $f->{"LINE"} and print;
$_ = $f->{"TEXT"} and print;
$_ = $f->{"MARK"} and print if $markpos eq RIGHT;
}
print "\n";
}
__END__
=pod
=head1 DESCRIPTION
B<sdif> is inspired by the System V L<sdiff(1)> command. The basic
feature of sdif is making a side-by-side listing of two different
files. All contents of two files are listed on left and right sides.
Center column is used to indicate how different those lines are. No
mark means no difference. Added, deleted and modified lines are
marked with minus C<-> and plus C<+> character, and wrapped line is
marked with period C<.>.
1 deleted -
2 same 1 same
3 changed -+ 2 modified
wrapped .. folded
4 same 3 same
+ 4 added
It also reads and formats the output from B<diff> command from
standard input. Besides normal diff output, context diff B<-c> and
unified diff B<-u> output will be handled properly. Combined diff and
conflict marker styles are also supported, but currently limited up to
three files.
The current implementation also supports C<#ifdef> and markdown custom
container (using seven colons) formats on an experimental basis. This
is to support the multilingual format generated by the
L<App::Greple::xlate> module.
If you want to just show multiple files side-by-side in parallel, and
do not concern about the difference of them, use L<App::ansicolumn>
command.
=head2 STARTUP and MODULE
B<sdif> utilizes Perl L<Getopt::EX> module, and reads C<~/.sdifrc>
file if available when starting up. You can define original and
default option there. To show the line number always, define like
this:
option default -n
Modules under B<App::sdif> can be loaded by B<-M> option without
prefix. Next command load B<App::sdif::colors> module.
$ sdif -Mcolors
You can also define options in module file. Read `perldoc
Getopt::EX::Module` for detail.
=head2 COLOR
Each lines are displayed in different colors by default. Use
B<--no-color> option to disable it. Each text segment has own labels,
and color for them can be specified by B<--colormap> option. Read
`perldoc Getopt::EX::Colormap` for detail.
Standard module B<-Mcolors> is loaded by default, and define several
color maps for light and dark screen. If you want to use CMY colors in
dark screen, place next line in your F<~/.sdifrc>.
option default --dark-cmy
Option B<--autocolor> is defined in B<default> module to call
L<Getopt::EX::termcolor> module. It sets B<--light> or B<--dark>
option according to the brightness of the terminal screen. You can
set preferred color in your F<~/.sdifrc> like:
option --light --cmy
option --dark --dark-cmy
Automatic setting is done by L<Getopt::EX::termcolor> module and it
works with macOS Terminal.app and iTerm.app, and other XTerm
compatible terminals. This module accept environment variable
L<TERM_BGCOLOR> as a terminal background color in a form of
C<#FFFFFF>.
Option B<--autocolor> is set by default, so override it to do nothing
to disable.
option --autocolor --nop
=head2 WORD DIFFERENCE
While B<sdif> doesn't care about the contents of each modified lines,
it can read the output from B<cdif> command which show the word
context differences of each lines. Use B<cdif> command with option
B<--sdif> to set the appropriate options for B<sdif>. Set B<--no-cc>,
B<--no-mc> options at least when invoking B<cdif> manually. Option
B<--no-tc> is preferable because text color can be handled by B<sdif>.
From version 4.1.0, option B<--cdif> is set by default, so use
B<--no-cdif> option to disable it. Option B<--unit> (default word)
will be passed through to B<cdif>. Other B<cdif> options can be
specified by B<--cdifopts>.
=head2 EXIT STATUS
B<sdif> always exit with status zero unless error occurred.
=head1 OPTIONS
=over 7
=item B<--width>=I<width>, B<-W> I<width>
Use width as a width of output listing. Default width is 80. If the
standard error is assigned to a terminal, the width is taken from it
if possible.
=item B<--margin>=I<column>
=item B<--runin>=I<column>
=item B<--runout>=I<column>
Set the number of margin column. Margin columns are left blank at the
end of each line. This option implicitly declare line break control,
which allows to run-in and run-out prohibited characters at the
head-and-end of line. Margin columns are used for run-in/run-out
columns unless they are given explicitly. See `perldoc
Text::ANSI::Fold` for detail.
=item B<-n>, B<-->[B<no->]B<number>
Print line number on each lines.
Default false.
=item B<-->[B<no->]B<command>
Print diff command control lines.
Default true.
=item B<-->[B<no->]B<filename>
Print filename lines.
Default true.
=item B<--digit>=I<n>
Line number is displayed in 4 digits by default. Use this option to
change it.
=item B<-i>, B<--ignore-case>
=item B<-b>, B<--ignore-space-change>
=item B<-w>, B<--ignore-all-space>
=item B<-B>, B<--ignore-blank-lines>
=item B<-c>, B<--context>=I<n>, B<-C>I<n>
=item B<-u>, B<--unified>=I<n>, B<-U>I<n>
Passed through to the back-end diff command. Sdif can interpret the
output from normal, context (B<diff -c>) and unified diff (B<diff
-u>).
=item B<-t>, B<-->[B<no->]B<truncate>
Truncate lines if they are longer than printing width.
Default false.
=item B<--boundary>=[C<none>,C<word>,C<space>]
Set text wrap boundary. If set as C<word> or C<space>, text is not
wrapped in the middle of alphanumeric word or non-space sequence. See
L<Text::ANSI::Fold> for detail.
Default is C<word>.
=item B<--onword>
Shortcut for B<--boundary=word>. No longer recommended to use.
Default true.
=item B<-->[B<no->]B<cdif>[=I<command>]
Use B<cdif> command instead of normal diff command. Enabled by
default and use B<--no-cdif> option explicitly to disable it. This
option accepts optional parameter as an actual B<cdif> command.
=item B<--cdifopts>=I<option>
Specify options for back-end B<cdif> command.
=item B<--unit>=[C<word>,C<letter>,C<char>,C<mecab>]
=item B<--by>=[C<word>,C<letter>,C<char>,C<mecab>]
=item B<--mecab>
These options are simply sent to back-end B<cdif> command. Choose
value from C<word> (default), C<letter>, C<char> or C<mecab>. Option
B<--by> is an alias for B<--unit>. Option B<--mecab> is a shortcut
for B<--unit=mecab>. Consult L<cdif> manual for detail.
Use B<--cdifopts> to set other options.
=item B<--diff>=I<command>
Any command can be specified as a diff command to be used. Piping
output to B<sdif> is easier unless you want to get whole text.
=item B<--diffopts>=I<option>
Specify options for back-end B<diff> command.
=item B<--mark>=I<position>
Specify the position for a mark. Choose from C<left>, C<right>,
C<center>, C<side> or C<no>. Default is C<center>.
=item B<--column>=I<order>
Specify the order of each column by B<O> (1: old), B<N> (2: new) and
B<M> (3: merged). Default order is "ONM" or "123". If you want to
show new file on left side and old file in right side, use like:
$ sdif --column NO
Next example show merged file on left-most column for diff3 data.
$ sdif --column MON
Next two commands produce same output.
$ git diff v1 v2 v3 | sdif --column 312
$ git diff v3 v1 v2 | sdif
=item B<-->[B<no->]B<color>
Use ANSI color escape sequence for output. Default is true.
=item B<-->[B<no->]B<256>
Use ANSI 256 color mode. Default is true.
=item B<--colortable>[=6,12,24]
Show table of ANSI 216 colors, when used without parameter.
Given the parameters, it displays 6x6, 12x12, and 24,24 color
matrices, respectively.
=item B<-v>, B<--view>
Viewer mode. Display each files in straightforward order. Without
this option, unchanged lines are placed at the same position.
=item B<-V>, B<--parallel>
=item B<-V3>, B<--parallel=3>
B<sdif> processes only looks-like-diff-output data and print anything
else as is to standard out. Option C<-V> or C<--parallel> makes
unknown text as common to old and new data. This is useful to see
file including conflict marker data compatible with L<git(1)>.
This option takes an optional number parameter and 2 is assumed when
omitted. Other than 2, only 3 is an effective value. L<diff3(1)>
command can produce conflict marker style output with C<-m> option.
So you can use like this:
$ diff3 -m A B C | sdif -V3
=item B<--ambiguous>=I<width_spec>
Specify the way to treat Unicode ambiguous width characters. Default
value is C<narrow>.
=over 4
=item B<detect> or B<auto>
Detect from user's locate. Set C<wide> when used in CJK environment.
=item B<wide> or B<full>
Treat ambiguous characters as wide.
=item B<narrow> or B<half>
Treat ambiguous characters as narrow.
=back
=item B<-->[B<no->]B<prefix>
Understand prefix for diff output including B<git> B<--graph> option.
True by default.
=item B<--prefix-pattern>=I<pattern>
Specify prefix pattern in regex. Default pattern is:
(?:\| )*(?: )?
This pattern matches B<git> graph style and whitespace indented diff
output.
=item B<-->[B<no->]B<lenience>
Suppress warning message for unexpected input from diff command. True
by default.
=item B<--visible> I<charname>=[0,1]
=item B<--tabhead>=I<char>
=item B<--tabspace>=I<char>
Visualize characters. Currently only C<ht> (horizontal tab) is
supported. Each horizontal tab character is converted to B<tabhead>
and following B<tabspace> characters. They can be specified by
B<--tabhead> and B<--tabspace> option.
$ sdif --visible ht=1 --tabhead=T --tabspace=.
If the option value is longer than single character, it is evaluated
as unicode name.
$ sdif --visible ht=1 \
--tabhead="MEDIUM SHADE" \
--tabspace="LIGHT SHADE"
names.
B<cdif> shows non-space control characters visible by default. See
L<cdif/--visible>.
=item B<--tabstyle>=[C<space>,C<dot>,C<symbol>,C<shade>,C<bar>,C<dash>...]
=item B<--ts>=...
Option B<--tabstyle> (or B<--ts>) allow to set B<--tabhead> and
B<--tabspace> characters at once according to the given style name.
Select from C<space>, C<dot>, C<symbol>, C<shade>, C<bar>, C<dash> and
others. See L<Text::ANSI::Fold/tabstyle> for available styles.
Multiple styles can be mixed up like C<symbol,space>. In this case,
tabhead and tabspace are taken from C<symbol> and C<space> style
respectively.
Setting tabstyle implies C<ht> being visible. If you want to set
tabstyle by default, but don't want to make tab visible always,
disable it explicitly.
option default --tabstyle=symbol,space --visible ht=0
Then you can enable it at the time of execution.
$ sdif --visible ht=1
=item B<--tabstop>=I<n>
Specify tab stop. Default is 8.
=item B<--colormap>=I<colormap>, B<--cm>=I<colormap>
Basic I<colormap> format is :
FIELD=COLOR
where the FIELD is one from these :
OLD NEW MERGED UNCHANGED
--------- --------- --------- ---------
OCOMMAND NCOMMAND MCOMMAND : Command line
OFILE NFILE MFILE : File name
OMARK NMARK MMARK UMARK : Mark
OLINE NLINE MLINE ULINE : Line number
OTEXT NTEXT MTEXT UTEXT : Text
If UMARK and/or ULINE is empty, OMARK/NMARK and/or OLINE/NLINE are
used instead.
You can make multiple fields same color joining them by = :
FIELD1=FIELD2=...=COLOR
Also wildcard can be used for field name :
*CHANGE=BDw
Multiple fields can be specified by repeating options
--cm FILED1=COLOR1 --cm FIELD2=COLOR2 ...
or combined with comma (,) :
--cm FILED1=COLOR1,FIELD2=COLOR2, ...
Color specification is a combination of single uppercase character
representing 8 colors :
R Red
G Green
B Blue
C Cyan
M Magenta
Y Yellow
K Black
W White
and alternative (usually brighter) colors in lowercase :
r, g, b, c, m, y, k, w
or RGB values and 24 grey levels if using ANSI 256 or full color
terminal :
(255,255,255) : 24bit decimal RGB colors
#000000 .. #FFFFFF : 24bit hex RGB colors
#000 .. #FFF : 12bit hex RGB 4096 colors
000 .. 555 : 6x6x6 RGB 216 colors
L00 .. L25 : Black (L00), 24 grey levels, White (L25)
or color names enclosed by angle bracket :
<red> <blue> <green> <cyan> <magenta> <yellow>
<aliceblue> <honeydue> <hotpink> <moccasin>
<medium_aqua_marine>
with other special effects :
D Double-struck (boldface)
I Italic
U Underline
S Stand-out (reverse video)
Above color spec is simplified summary so if you want complete
information, read L<Getopt::EX::Colormap>.
Defaults are :
OCOMMAND => "555/010" or "GS"
NCOMMAND => "555/010" or "GS"
MCOMMAND => "555/010" or "GS"
OFILE => "551/010D" or "GDS"
NFILE => "551/010D" or "GDS"
MFILE => "551/010D" or "GDS"
OMARK => "010/444" or "G/W"
NMARK => "010/444" or "G/W"
MMARK => "010/444" or "G/W"
UMARK => ""
OLINE => "220" or "Y"
NLINE => "220" or "Y"
MLINE => "220" or "Y"
ULINE => ""
OTEXT => "K/454" or "G"
NTEXT => "K/454" or "G"
MTEXT => "K/454" or "G"
UTEXT => ""
This is equivalent to :
sdif --cm '?COMMAND=555/010,?FILE=555/010D' \
--cm '?MARK=010/444,UMARK=' \
--cm '?LINE=220,ULINE=' \
--cm '?TEXT=K/454,UTEXT='
=item B<--colormap>=C<&func>
=item B<--colormap>=C<sub{...}>
You can also set the name of perl subroutine name or definition to be
called handling matched words. Target word is passed as variable
C<$_>, and the return value of the subroutine will be displayed.
See L<Getopt::EX::Colormap/FUNCTION SPEC> for detail.
=item B<-->[B<no->]B<cc>, B<-->[B<no->]B<commandcolor>
=item B<-->[B<no->]B<fc>, B<-->[B<no->]B<filecolor>
=item B<-->[B<no->]B<lc>, B<-->[B<no->]B<linecolor>
=item B<-->[B<no->]B<mc>, B<-->[B<no->]B<markcolor>
=item B<-->[B<no->]B<tc>, B<-->[B<no->]B<textcolor>
=item B<-->[B<no->]B<uc>, B<-->[B<no->]B<unknowncolor>
Enable/Disable using color for the corresponding field.
=back
=head1 MODULE OPTIONS
=head2 default
default --autocolor
--nop do nothing
=head2 -Mcolors
Following options are available by default. Use `perldoc -m
App::sdif::colors` to see actual setting.
--light
--green
--cmy
--mono
--dark
--dark-green
--dark-cmy
--dark-mono
=head1 ENVIRONMENT
Environment variable B<SDIFOPTS> is used to set default options.
=head1 AUTHOR
=over
=item Kazumasa Utashiro
=back
=head1 LICENSE
Copyright 1992-2025 Kazumasa Utashiro
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=head1 SEE ALSO
L<cdif(1)>, L<watchdiff(1)>
L<Getopt::EX::Colormap>
L<Getopt::EX::termcolor>
L<App::sdif::colors>
L<App::ansicolumn>
L<App::Greple::xlate>
=cut
# LocalWords: perldoc colormap autocolor termcolor onword cdifopts
# LocalWords: mecab CJK diffopts colortable Unicode OCOMMAND cdif
# LocalWords: NCOMMAND OFILE MCOMMAND NFILE MFILE OMARK NMARK MMARK
# LocalWords: UMARK OLINE NLINE MLINE ULINE OTEXT NTEXT MTEXT UTEXT
# LocalWords: Cyan RGB SDIFOPTS Kazumasa Utashiro watchdiff sdif
# LocalWords: sdiff diff sdifrc CMY cmy macOS iTerm XTerm runin
# LocalWords: runout regex lenience tabhead tabspace unicode cyan
# LocalWords: tabstyle tabstop perl