#!/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.39 =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 warnings; use utf8; use Encode; use open IO => ':utf8'; use Carp; use charnames ':full'; use List::Util qw(min max reduce sum pairmap first); use Pod::Usage; use Text::ParseWords qw(shellwords); use Data::Dumper; { $Data::Dumper::Terse = 1; } use App::sdif; my $version = $App::sdif::VERSION; use App::sdif::Util; 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; use Getopt::EX::Hashed; { 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 => "" , ); }; use Getopt::EX::Colormap; $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 $/; }; 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 () { # # 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 = ; print_command_n({ type => 'FILE' }, $_, $next); } elsif ($_ eq "***************\n") { my(@old, @new); my $ohead = $_ = ; my($left, $right); unless (($left) = /^\*\*\* ([\d,]+) \*\*\*\*$/) { print; next; } my $oline = range($left); my $dline = 0; my $cline = 0; my $nhead = $_ = ; unless (($right) = /^--- ([\d,]+) ----$/) { @old = read_line(*DIFF, $oline - 1, $nhead); $nhead = $_ = ; 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 = ; 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_re) (? \@\@ [ ] \-(?\d+) (?:,(?\d+))? [ ] \+(?\d+) (?:,(?\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; use App::sdif::Divert; $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 = ; 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 = ; my $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 is inspired by the System V L 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 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 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 command. =head2 STARTUP and MODULE B utilizes Perl L 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 can be loaded by B<-M> option without prefix. Next command load B 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 module to call L 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 module and it works with macOS Terminal.app and iTerm.app, and other XTerm compatible terminals. This module accept environment variable L 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 doesn't care about the contents of each modified lines, it can read the output from B command which show the word context differences of each lines. Use B command with option B<--sdif> to set the appropriate options for B. Set B<--no-cc>, B<--no-mc> options at least when invoking B manually. Option B<--no-tc> is preferable because text color can be handled by B. 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. Other B options can be specified by B<--cdifopts>. =head2 EXIT STATUS B always exit with status zero unless error occurred. =head1 OPTIONS =over 7 =item B<--width>=I, B<-W> I 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 =item B<--runin>=I =item B<--runout>=I 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]B Print line number on each lines. Default false. =item B<-->[B]B Print diff command control lines. Default true. =item B<-->[B]B Print filename lines. Default true. =item B<--digit>=I 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, B<-C>I =item B<-u>, B<--unified>=I, B<-U>I Passed through to the back-end diff command. Sdif can interpret the output from normal, context (B) and unified diff (B). =item B<-t>, B<-->[B]B Truncate lines if they are longer than printing width. Default false. =item B<--boundary>=[C,C,C] Set text wrap boundary. If set as C or C, text is not wrapped in the middle of alphanumeric word or non-space sequence. See L for detail. Default is C. =item B<--onword> Shortcut for B<--boundary=word>. No longer recommended to use. Default true. =item B<-->[B]B[=I] Use B 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 command. =item B<--cdifopts>=I