File Coverage

script/sdif
Criterion Covered Total %
statement 410 640 64.0
branch 128 282 45.3
condition 61 171 35.6
subroutine 35 66 53.0
pod n/a
total 634 1159 54.7


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2              
3             ##
4             ## sdif: sdiff clone
5             ##
6             ## Copyright (c) 1992- Kazumasa Utashiro
7             ##
8             ## Original version on Jul 24 1991
9             ##
10              
11             =pod
12              
13             =head1 NAME
14              
15             sdif - side-by-side diff viewer for ANSI terminal
16              
17             =head1 VERSION
18              
19             Version 4.44
20              
21             =head1 SYNOPSIS
22              
23             sdif file_1 file_2
24              
25             diff ... | sdif
26              
27             OPTIONS:
28              
29             -i, --ignore-case
30             -b, --ignore-space-change
31             -w, --ignore-all-space
32             -B, --ignore-blank-lines
33              
34             --[no]number, -n print line number
35             --digit=# set the line number digits (default 4)
36             --truncate, -t truncate long line
37             --boundary=# line folding boundary (default word)
38             --context, -c, -C# context diff
39             --unified, -u, -U# unified diff
40              
41             --width=#, -W# width of output (default 80)
42             --margin=# margin column number (default 0)
43             --runin=# run-in column number (default --margin)
44             --runout=# run-out column number (default --margin)
45             --mark=position mark position (right, left, center, side) or no
46             --column=order column order (default ONM)
47             --view, -v viewer mode
48             --parallel[=#], -V treat unknown text as common part (default 2)
49             --ambiguous=s ambiguous character width (detect, wide, narrow)
50             --[no-]command print diff control command (default on)
51             --[no-]filename print diff filename (default on)
52             --[no-]prefix process git --graph output (default on)
53             --prefix-pattern prefix pattern
54              
55             --color=when 'always' (default), 'never' or 'auto'
56             --nocolor --color=never
57             --colormap, --cm specify color map
58             --colortable[=#] show color table (optional #: 6, 12, 24)
59             --[no-]256 on/off ANSI 256 color mode (default on)
60             --[no-]cc color command line (default true)
61             --[no-]fc file name (default true)
62             --[no-]lc line number (default true)
63             --[no-]mc diff mark (default true)
64             --[no-]tc normal text (default true)
65             --[no-]uc unknown text (default true)
66              
67             --man display manual page
68             --version show version
69             --diff=s set diff command
70             --diffopts=s set diff command options
71              
72             --[no-]lenience suppress unexpected input warning (default on)
73             --visible xx=1 set visible chars
74             --tabhead=char set tabhead char
75             --tabspace=char set tabspace char
76             --tabstyle=style set tabstyle (dot, symbol, shade, bar, dash...)
77             --tabstop=# set tabstop width (default 8)
78              
79             --[no-]cdif use ``cdif'' as word context diff backend
80             --unit=s pass through to cdif (word, letter, char, mecab)
81             --cdifopts=s set cdif command options
82              
83             =cut
84              
85 5     5   31595 use v5.14;
  5         19  
86 5     5   30 use warnings;
  5         8  
  5         466  
87 5     5   3016 use utf8;
  5         1904  
  5         36  
88 5     5   5352 use Encode;
  5         136622  
  5         857  
89 5     5   3423 use open IO => ':utf8';
  5         8345  
  5         32  
90 5     5   624 use Carp;
  5         9  
  5         441  
91 5     5   3166 use charnames ':full';
  5         60188  
  5         45  
92 5     5   144782 use List::Util qw(min max reduce sum pairmap first);
  5         21  
  5         1048  
93 5     5   3171 use Pod::Usage;
  5         330578  
  5         907  
94 5     5   3403 use Text::ParseWords qw(shellwords);
  5         10217  
  5         439  
95 5     5   3739 use Data::Dumper; {
  5         47963  
  5         673  
96 5         1142067 $Data::Dumper::Terse = 1;
  5         19  
97             }
98              
99 5     5   3046 use App::sdif;
  5         14  
  5         519  
100 5         27 my $version = $App::sdif::VERSION;
101              
102 5     5   2899 use App::sdif::Util;
  5         15  
  5         937  
103 5     5   3095 use IO::Divert;
  5         4923  
  5         957  
104              
105 5         15 my $default_cdif = 'cdif';
106 5         17 my @default_cdifopts = qw(--sdif);
107 5         13 my $default_lenience = 1;
108 5         12 my $default_256 = 1;
109 5         10 my $default_prefix = 1;
110 5         13 my $default_prefix_pattern = q/(?:\\| )*(?: )?/;
111 5         36 my @cdifopts;
112             my $read_stdin;
113              
114 5         12 our $screen_width;
115              
116 5 50       31 if (my $env = $ENV{'SDIFOPTS'}) {
117 0         0 unshift @ARGV, shellwords($env);
118             }
119              
120 5     5   43 use open IO => ':utf8', ':std';
  5         8  
  5         48  
121 5 50       15 map { $_ = decode 'utf8', $_ unless utf8::is_utf8($_) } @ARGV;
  9         268  
122              
123 5         121 my $app;
124              
125 5     5   4410 use Getopt::EX::Hashed; {
  5         51833  
  5         38  
126              
127 5         12 Getopt::EX::Hashed->configure( DEFAULT => [ is => 'rw' ] );
  5         69  
128              
129 5         229 has help => ' h ' ;
130 5         347 has version => ' ' ;
131 5         316 has man => ' ' ;
132 5         254 has debug => 'd + ' ;
133 5         230 has number => 'n ! ' ;
134 5         234 has digit => ' =i ' , default => 4 ;
135 5         321 has column => ' =s ' ;
136 5         289 has truncate => 't ! ' ;
137 5         303 has boundary => ' =s ' , default => 'word' ;
138 5         290 has onword => ' ! ' ;
139 5         307 has mark => ' =s ' , default => 'center' ;
140 5         294 has prefix => ' ! ' , default => $default_prefix ;
141 5         311 has prefix_pattern => ' =s ' , default => $default_prefix_pattern ;
142 5         309 has width => 'W =i ' ;
143 5         268 has margin => ' =i ' , default => 0 ;
144 5         298 has runin => ' =i ' ;
145 5         286 has runout => ' =i ' ;
146 5         284 has view => 'v ! ' ;
147 5         290 has parallel => 'V :2 ' , default => 0 ;
148 5         423 has ambiguous => ' =s ' , default => 'narrow' ;
149 5         369 has filename => ' ! ' , default => 1 ;
150 5         409 has command => ' ! ' , default => 1 ;
151 5         329 has diff => ' =s ' , default => 'diff' ;
152 5         334 has diffopts => ' =s@' , default => [] ;
153 5         409 has color => ' =s ' , default => 'always' ;
154 5         419 has colormap => 'cm=s@' , default => [] ;
155 5         379 has colordump => ' ' ;
156 5         479 has 256 => ' ! ' , default => $default_256 ;
157 5         439 has commandcolor => 'cc! ' , default => 1 ;
158 5         354 has filecolor => 'fc! ' , default => 1 ;
159 5         454 has linecolor => 'lc! ' , default => 1 ;
160 5         416 has markcolor => 'mc! ' , default => 1 ;
161 5         396 has textcolor => 'tc! ' , default => 1 ;
162 5         369 has unknowncolor => 'uc! ' , default => 1 ;
163 5         385 has cdif => ' :s ' , default => '' ;
164 5         451 has cdifopts => ' =s ' ;
165 5         401 has colortable => ' :s ' , any => qr/^(|6|12|24)$/ ;
166 5         419 has lenience => ' ! ' , default => $default_lenience ;
167 5         464 has visible => ' =i%' , default => {} ;
168 5         461 has tabstop => ' =i ' , default => 8 ;
169 5         527 has tabhead => ' =s ' ;
170 5         475 has tabstyle => 'ts=s ' ;
171 5         504 has tabspace => ' =s ' ;
172 5         464 has unit => 'by:s ' ;
173              
174 5         538 has ignore_case => 'i ' ;
175 5         606 has ignore_space_change => 'b ' ;
176 5         459 has ignore_all_space => 'w ' ;
177 5         439 has ignore_blank_lines => 'B ' ;
178 5         497 has context => 'C =i' ;
179 5         460 has unified => 'U =i' ;
180 5         453 has c => ' ' ;
181 5         532 has u => ' ' ;
182              
183             has '+onword'
184 5 0   0   555 => sub { $_->boundary = $_[1] ? 'word' : '' } ;
  0         0  
185              
186             has '+cdifopts'
187 5     0   303 => sub { push @cdifopts, shellwords $_[1] } ;
  0         0  
188              
189             has '+ignore_case'
190 5     0   447 => sub { push @{$app->diffopts}, '-i'; push @cdifopts, '-i' } ;
  0         0  
  0         0  
  0         0  
191             has '+ignore_space_change'
192 5     0   627 => sub { push @{$app->diffopts}, '-b'; push @cdifopts, '-w' } ;
  0         0  
  0         0  
  0         0  
193             has '+ignore_all_space'
194 5     0   545 => sub { push @{$app->diffopts}, '-w'; push @cdifopts, '-w' } ;
  0         0  
  0         0  
  0         0  
195             has '+ignore_blank_lines'
196 5     0   475 => sub { push @{$app->diffopts}, '-B' } ;
  0         0  
  0         0  
197              
198 5     0   443 has '+c' => sub { push @{$app->diffopts}, '-c' } ;
  0         0  
  0         0  
199 5     0   532 has '+u' => sub { push @{$app->diffopts}, '-u' } ;
  0         0  
  0         0  
200 5     0   567 has '+context' => sub { push @{$app->diffopts}, '-C' . $_[1] } ;
  0         0  
  0         0  
201 5     0   502 has '+unified' => sub { push @{$app->diffopts}, '-U' . $_[1] } ;
  0         0  
  0         0  
202              
203 5     0   461 has nocolor => 'no-color' , action => sub { $app->color = 'never' } ;
  0         0  
204 5     0   583 has nocdif => 'no-cdif' , action => sub { $app->cdif = undef } ;
  0         0  
205              
206 5 0   0   619 has mecab => '!' , action => sub { $app->unit = $_[1] ? 'mecab' : undef } ;
  0         0  
207              
208             has '+ambiguous' => sub {
209 0 0   0   0 if ($_[1] =~ /^(?:wide|full)/) {
210 0         0 $Text::VisualWidth::PP::EastAsian = 1;
211 0         0 Text::ANSI::Fold->configure(ambiguous => 'wide');
212             }
213 5         605 } ;
214              
215 5     0   332 has '+help' => sub { usage() } ;
  0         0  
216 5     0   215 has '+version' => sub { print "$version\n"; exit 0 } ;
  0         0  
  0         0  
217 5     0   191 has '+man' => sub { pod2usage {-verbose => 2} } ;
  0         0  
218              
219 5     5   9404 } no Getopt::EX::Hashed;
  5         14  
  5         27  
220              
221 5 50       235 $app = Getopt::EX::Hashed->new() or die;
222              
223 5         11531 my @SAVEDARGV = @ARGV;
224 5     5   3669 use Getopt::EX::Long qw(:DEFAULT Configure ExConfigure);
  5         583156  
  5         1591  
225 5         49 ExConfigure BASECLASS => [ "App::sdif", "Getopt::EX" ];
226 5         219 Configure "bundling";
227 5 50       314 $app->getopt or usage({status => 1});
228              
229 5 50       105889 warn "\@ARGV = (@SAVEDARGV)\n" if $app->debug;
230              
231 5         55 $App::sdif::Util::NO_WARNINGS = $app->lenience;
232              
233 5     5   3198 use Text::VisualWidth::PP qw(vwidth);
  5         21738  
  5         513  
234 5     5   3168 use Text::ANSI::Fold qw(ansi_fold :constants); {
  5         94963  
  5         4801  
235 5         31 Text::ANSI::Fold->configure(padding => 1,
  5         23  
236             expand => 1,
237             tabstop => $app->tabstop);
238             }
239              
240 5 50 0     451 $app->visible->{ht} //= 1 if $app->tabstyle;
241 5 50       55 if ($app->visible->{ht}) {
242             Text::ANSI::Fold->configure(
243             tabstyle => $app->tabstyle,
244 0         0 map { $_->[0] => unicode($_->[1]) }
245 0         0 grep { $_->[1] }
  0         0  
246             [ tabhead => $app->tabhead ],
247             [ tabspace => $app->tabspace ],
248             );
249             }
250              
251             sub unicode {
252 0 0   0   0 my $char = shift or return undef;
253 0 0       0 if ($char =~ /^\X$/) {
254 0         0 $char;
255             } else {
256 0 0       0 eval qq["\\N{$char}"] or die "$!";
257             }
258             }
259              
260 5 50       46 if ($app->margin > 0) {
261 0 0 0     0 if ($app->runin and $app->margin < $app->runin) {
262 0         0 die "margin must be >= runin\n";
263             }
264             Text::ANSI::Fold->configure(
265 0   0     0 linebreak => LINEBREAK_ALL,
      0        
266             margin => $app->margin,
267             runin => $app->runin // $app->margin,
268             runout => $app->runout // $app->margin,
269             );
270             }
271              
272 5         36 my %colormap = do {
273 5 50       28 my $col = $app->{256} ? 0 : 1;
274 95 100   95   320 pairmap { $a => (ref $b eq 'ARRAY') ? $b->[$col] : $b } (
275 5         154 UNKNOWN => "" ,
276             OCOMMAND => [ "555/010" , "GS" ],
277             NCOMMAND => [ "555/010" , "GS" ],
278             MCOMMAND => [ "555/010" , "GS" ],
279             OFILE => [ "551/010D" , "GDS" ],
280             NFILE => [ "551/010D" , "GDS" ],
281             MFILE => [ "551/010D" , "GDS" ],
282             OMARK => [ "010/444" , "G/W" ],
283             NMARK => [ "010/444" , "G/W" ],
284             MMARK => [ "010/444" , "G/W" ],
285             UMARK => "" ,
286             OLINE => [ "220" , "Y" ],
287             NLINE => [ "220" , "Y" ],
288             MLINE => [ "220" , "Y" ],
289             ULINE => "" ,
290             OTEXT => [ "K/454" , "G" ],
291             NTEXT => [ "K/454" , "G" ],
292             MTEXT => [ "K/454" , "G" ],
293             UTEXT => "" ,
294             );
295             };
296              
297 5     5   57 use Getopt::EX::Colormap;
  5         13  
  5         411  
298 5         36 $Getopt::EX::Colormap::NO_RESET_EL = 1;
299 5     5   31 use constant SGR_RESET => "\e[m";
  5         12  
  5         6730  
300             my $color_handler = Getopt::EX::Colormap
301             ->new(HASH => \%colormap)
302 5         58 ->load_params(@{$app->colormap});
  5         442  
303              
304 5   33     254 $colormap{OUMARK} ||= $colormap{UMARK} || $colormap{OMARK};
      33        
305 5   33     58 $colormap{NUMARK} ||= $colormap{UMARK} || $colormap{NMARK};
      33        
306 5   33     55 $colormap{OULINE} ||= $colormap{ULINE} || $colormap{OLINE};
      33        
307 5   33     71 $colormap{NULINE} ||= $colormap{ULINE} || $colormap{NLINE};
      33        
308              
309 5         27 for (
310             [ $app->unknowncolor => q/UNKNOWN/ ],
311             [ $app->commandcolor => q/COMMAND/ ],
312             [ $app->filecolor => q/FILE/ ],
313             [ $app->linecolor => q/LINE/ ],
314             [ $app->markcolor => q/MARK/ ],
315             [ $app->textcolor => q/TEXT/ ],
316             ) {
317 30         198 my($color, $label) = @$_;
318 30 50       98 $color and next;
319 0         0 for (grep /$label/, keys %colormap) {
320 0         0 $colormap{$_} = '';
321             }
322             }
323              
324 5 50       31 if ($app->colordump) {
325 0         0 print $color_handler->colormap(
326             name => '--changeme', option => '--colormap');
327 0         0 exit;
328             }
329              
330 5         36 my $painter = do {
331 5 50 0     20 if (($app->color eq 'always')
      33        
332             or (($app->color eq 'auto') and (-t STDOUT))) {
333 5     445   88 sub { $color_handler->color(@_) };
  445         1493  
334             } else {
335 0     0   0 sub { $_[1] } ;
  0         0  
336             }
337             };
338              
339             ##
340             ## setup cdif command and option
341             ##
342 5 50 33     22 if (defined $app->cdif and $app->cdif eq '') {
343 5         77 $app->cdif = $default_cdif;
344             }
345              
346 5         37 for (
347             [ "unit" , "=" , $app->unit , undef ] ,
348             [ "256" , "!" , $app->{256} , $default_256 ] ,
349             [ "prefix" , "!" , $app->prefix , $default_prefix ] ,
350             [ "lenience" , "!" , $app->lenience , $default_lenience ] ,
351             )
352             {
353 20         143 my($name, $type, $var, $default) = @$_;
354 20 100       75 if ($type eq "!") {
    50          
355 15 50       45 next if not defined $var;
356 15 50       79 next if $var == $default;
357 0 0       0 unshift @cdifopts, sprintf("--%s%s", $var ? '' : 'no-', $name);
358             } elsif ($type eq "=") {
359 5 50       32 next if not defined $var;
360 0         0 unshift @cdifopts, sprintf("--%s=%s", $name, $var);
361             } else {
362 0         0 die;
363             }
364             }
365              
366 5         29 unshift @cdifopts, @default_cdifopts;
367              
368 5         15 my($OLD, $NEW, $DIFF);
369 5 50       29 if (@ARGV == 2) {
    50          
370 0         0 ($OLD, $NEW) = @ARGV;
371 0         0 $DIFF = "$app->{diff} @{$app->{diffopts}} $OLD $NEW |";
  0         0  
372             } elsif (@ARGV < 2) {
373 5   100     24 $DIFF = shift || '-';
374 5         11 $read_stdin++;
375             } else {
376 0         0 usage({status => 1}, "Unexpected arguments.\n\n");
377             }
378             my $readfile =
379 5   0     23 ($OLD and $NEW) && !$read_stdin && !(grep { /^-[cuCU]/ } @{$app->diffopts});
380              
381             use constant {
382 5         2118 RIGHT => 'right',
383             LEFT => 'left',
384             NO => 'no',
385 5     5   54 };
  5         9  
386 5         69 my %markpos = (
387             center => [ RIGHT , LEFT , LEFT ],
388             side => [ LEFT , RIGHT , LEFT ],
389             right => [ RIGHT , RIGHT , RIGHT ],
390             left => [ LEFT , LEFT , LEFT ],
391             no => [ NO , NO , NO ],
392             none => [ NO , NO , NO ],
393             );
394 5 50       26 unless ($markpos{$app->mark}) {
395 0         0 my @keys = sort keys %markpos;
396 0         0 usage "Use one from (@keys) for option --mark\n\n";
397             }
398 5         39 my @markpos = @{$markpos{$app->mark}};
  5         16  
399 5         36 my($omarkpos, $nmarkpos, $mmarkpos) = @markpos;
400              
401 5         40 my $num_format = sprintf '%%%dd', $app->digit;
402              
403 5   66     49 $screen_width = $app->width || &terminal_width;
404              
405             sub column_width {
406 114     114   328 my $column = shift;
407 114         158 state %column_width;
408 114   66     690 $column_width{$screen_width * 1000 + $column} //= do {
409 5     5   41 use integer;
  5         22  
  5         34  
410 4         16 my $w = $screen_width;
411 4 50       339 $w -= $column if $app->mark;
412 4         117 max 1, $w / $column;
413             };
414             }
415              
416             ##
417             ## --colortable
418             ##
419 5 100       38 if (defined(my $n = $app->colortable)) {
420 5     5   809 no strict 'refs';
  5         12  
  5         71645  
421 1         6 &{"Getopt::EX::Colormap::colortable$n"}($screen_width);
  1         15  
422 1         0 exit;
423             }
424              
425             ##
426             ## Column order
427             ##
428 4 50       39 my @column = !$app->column ? () : do {
429 0         0 map { $_ - 1 }
430 0   0     0 map { { O=>1, N=>2, M=>3 }->{$_} // $_ }
  0         0  
431             $app->column =~ /[0-9ONM]/g;
432             };
433              
434             ##
435             ## Git --graph prefix pattern
436             ##
437 4         26 my $prefix_re = do {
438 4 50       14 if ($app->prefix) {
439 4         158 qr/$app->{prefix_pattern}/;
440             } else {
441 0         0 "";
442             }
443             };
444              
445 4 50       21 if ($app->debug) {
446 0   0     0 printf STDERR "\$OLD = %s\n", $OLD // "undef";
447 0   0     0 printf STDERR "\$NEW = %s\n", $NEW // "undef";
448 0   0     0 printf STDERR "\$DIFF = %s\n", $DIFF // "undef";
449             }
450              
451 4 50       34 if ($app->cdif) {
452 4         12756 my $pid = open DIFF, '-|';
453 4 50       873 if (not defined $pid) {
    50          
454 0 0       0 die "$!" if not defined $pid;
455             }
456             ## child
457             elsif ($pid == 0) {
458 0 0       0 if ($DIFF ne '-') {
459 0 0       0 open(STDIN, $DIFF) || die "cannot open diff: $!\n";
460             }
461 0         0 do { exec shellwords($app->cdif), @cdifopts } ;
  0         0  
462 0         0 warn "exec failed: $!";
463 0         0 print while <>;
464 0         0 exit;
465             }
466             ## parent
467             else {
468             ## nothing to do
469             }
470             } else {
471 0 0       0 open(DIFF, $DIFF) || die "cannot open diff: $!\n";
472             }
473              
474 4 50       339 if ($readfile) {
475              
476 0         0 binmode DIFF, ':raw';
477 0         0 my $DIFFOUT = do { local $/; };
  0         0  
  0         0  
478 0         0 close DIFF;
479 0 0       0 open DIFF, '<', \$DIFFOUT or die;
480              
481 0 0       0 open OLD, $OLD or die "$OLD: $!\n";
482 0 0       0 open NEW, $NEW or die "$NEW: $!\n";
483              
484             # For reading /dev/fd/*
485 0 0 0     0 seek OLD, 0, 0 or die unless -p OLD;
486 0 0 0     0 seek NEW, 0, 0 or die unless -p NEW;
487             }
488              
489 4         1356 my @boundary = (boundary => $app->boundary);
490 4         562 my $color_re = qr{ \e \[ [\d;]* [mK] }x;
491              
492 4         84 my $oline = 1;
493 4         64 my $nline = 1;
494 4         22 my $mline = 1;
495              
496 4         3642333 while () {
497             #
498             # normal diff
499             #
500 40 100       1472 if (/^([\d,]+)([adc])([\d,]+)$/) {
    100          
    100          
    100          
    100          
    50          
    50          
    50          
    50          
501 5         16 my(@old, @new);
502 5         77 my($left, $ctrl, $right) = ($1, $2, $3);
503 5         58 my($l1, $l2) = range($left);
504 5         21 my($r1, $r2) = range($right);
505 5 50       25 if ($readfile) {
506 0         0 my $identical_line = $l1 - $oline + 1 - ($ctrl ne 'a');
507 0         0 print_identical($identical_line);
508             }
509 5 50 33     36 if ($app->debug || $read_stdin) {
510 5         297 print_command_n($_, $_);
511             }
512 5 100 100     34 if ($ctrl eq 'd' || $ctrl eq 'c') {
513 4         28 ($oline) = $left =~ /^(\d+)/;
514 4         17 my $n = $l2 - $l1 + 1;
515 4         52 @old = read_line(*DIFF, $n);
516 4 50       12 $readfile and read_line(*OLD, $n);
517             }
518 5 100       22 read_line(*DIFF, 1) if $ctrl eq 'c';
519 5 100 100     50 if ($ctrl eq 'a' || $ctrl eq 'c') {
520 4         49 ($nline) = $right =~ /^(\d+)/;
521 4         14 my $n = $r2 - $r1 + 1;
522 4         17 @new = read_line(*DIFF, $n);
523 4 50       12 $readfile and read_line(*NEW, $n);
524             }
525             map {
526 5         13 s/^([<>])\s?/{'<' => '-', '>' => '+'}->{$1}/e
  12         61  
  12         97  
527             } @old, @new;
528 5         27 flush_buffer([], \@old, \@new);
529             }
530             #
531             # context diff
532             #
533             elsif (/^\*\*\* /) {
534 1         8 my $next = ;
535 1         64 print_command_n({ type => 'FILE' }, $_, $next);
536             }
537             elsif ($_ eq "***************\n") {
538 1         5 my(@old, @new);
539 1         17380 my $ohead = $_ = ;
540 1         10 my($left, $right);
541 1 50       46 unless (($left) = /^\*\*\* ([\d,]+) \*\*\*\*$/) {
542 0         0 print;
543 0         0 next;
544             }
545 1         21 my $oline = range($left);
546 1         4 my $dline = 0;
547 1         5 my $cline = 0;
548 1         6 my $nhead = $_ = ;
549 1 50       5 unless (($right) = /^--- ([\d,]+) ----$/) {
550 1         14 @old = read_line(*DIFF, $oline - 1, $nhead);
551 1         2 $nhead = $_ = ;
552 1 50       18 unless (($right) = /^--- ([\d,]+) ----$/) {
553 0         0 print $ohead, @old, $_;
554 0         0 next;
555             }
556 1         5 for (@old) {
557 14 100       19 /^-/ and ++$dline;
558 14 100       20 /^!/ and ++$cline;
559             }
560             }
561 1         3 my $nline = range($right);
562 1 50 33     14 if (@old == 0 or $cline != 0 or ($oline - $dline != $nline)) {
      33        
563 1         4 @new = read_line(*DIFF, $nline);
564             }
565 1         9 print_command_n($ohead, $nhead);
566 1         6 ($oline) = $left =~ /^(\d+)/;
567 1         3 ($nline) = $right =~ /^(\d+)/;
568              
569 1         13 my @buf = merge_diffc(\@old, \@new);
570 1         7 flush_buffer(@buf);
571             }
572             #
573             # unified diff
574             #
575             elsif (/^($prefix_re)(--- (?s:.*))/) {
576 5         90 my($prefix, $left) = ($1, $2);
577 5         29 my $right = ;
578 5         32 local $screen_width = $screen_width;
579 5 100       144 if ($prefix) {
580 4         73 $right =~ s/^\Q$prefix//;
581 4         14 print $prefix;
582 4         11 $screen_width -= length $prefix;
583             }
584 5         92 print_command_n({ type => 'FILE' }, $left, $right);
585             }
586             elsif (m{^
587             (?$prefix_re)
588             (?
589             \@\@ [ ]
590             \-(?\d+) (?:,(?\d+))? [ ]
591             \+(?\d+) (?:,(?\d+))? [ ]
592             \@\@
593             (?s:.*)
594             )
595             }x) {
596 5         107 ($oline, $nline) = @+{qw(oline nline)};
597 5   50     81 my($o, $n) = ($+{o}//1, $+{n}//1);
      50        
598 5         47 my($prefix, $command) = @+{qw(prefix command)};
599              
600 5         22 local $screen_width = $screen_width;
601 5         11 my($divert, %read_opt);
602 5 100       82 if ($prefix) {
603 4         14 $screen_width -= length $prefix;
604 4         17 $read_opt{prefix} = $prefix;
605 4     4   67 $divert = IO::Divert->new(FINAL => sub { s/^/$prefix/mg });
  4         389  
606             }
607              
608 5         3360 print_command_n({ type => 'COMMAND' }, $command, $command);
609 5         63 my @buf = read_unified_2 \%read_opt, *DIFF, $o, $n;
610              
611 5         113 flush_buffer(@buf);
612             }
613             #
614             # diff --combined (only support 3 files)
615             #
616             elsif (/^diff --(?:cc|combined)/) {
617 0         0 my @lines = ($_);
618 0     0   0 push @lines, read_until { /^\+\+\+/ } *DIFF;
  0         0  
619 0 0       0 if (not defined $lines[-1]) {
620 0         0 pop @lines;
621 0         0 print @lines;
622 0         0 next;
623             }
624 0         0 print @lines;
625             }
626             elsif (/^\@{3} -(\d+)(?:,(\d+))? -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? \@{3}/) {
627 0         0 print_command_n({ type => 'COMMAND' }, $_, $_, $_);
628              
629 0         0 ($oline, $nline, $mline) = ($1, $3, $5);
630 0         0 state $read_unified_3 = read_unified_sub(3);
631 0   0     0 my @buf = $read_unified_3->(*DIFF, $2 // 1, $4 // 1, $6 // 1);
      0        
      0        
632 0         0 flush_buffer_3(@buf);
633             }
634             #
635             # conflict marker
636             #
637             elsif (/^<<<<<<<\h*+(.*)/) {
638             CONFLICT:
639             {
640 0         0 my %name = (o => $_);
  0         0  
641 0         0 my($c1, $c2, $c3, $c4);
642 0         0 $c1 = $_;
643              
644 0     0   0 my @old = read_until { /^=======$/ } *DIFF;
  0         0  
645 0   0     0 $c2 = pop @old // do {
646 0         0 flush_unknown($c1, @old);
647 0         0 last;
648             };
649              
650 0     0   0 my @new = read_until { /^>>>>>>>\h*+(.*)/ } *DIFF;
  0         0  
651 0   0     0 $c4 = pop @new // do {
652 0         0 flush_unknown($c1, @old, $c2, @new);
653 0         0 last;
654             };
655 0         0 $name{n} = $c4;
656              
657 0         0 my @mrg;
658 0     0   0 my $mrg = first { $old[$_] =~ /^\Q|||||||\E\h*+(.*)/ } keys @old;
  0         0  
659 0 0 0     0 if (defined $mrg or $app->parallel > 2) {
660 0 0       0 if (defined $mrg) {
661 0         0 $name{m} = $old[$mrg];
662 0         0 ($c3, @mrg) = splice @old, $mrg;
663             } else {
664 0         0 $name{m} = $name{o};
665 0         0 $name{o} = $name{n};
666 0         0 @mrg = @old;
667 0         0 @old = @new;
668             }
669 0         0 s/^/--/ for @old, @mrg;
670 0         0 s/^/++/ for @new;
671 0         0 print_command_n({ type => 'FILE' }, @name{qw(o m n)});
672 0         0 flush_buffer_3([], \@old, \@mrg, \@new);
673             } else {
674 0         0 s/^/-/ for @old;
675 0         0 s/^/+/ for @new;
676 0         0 print_command_n({ type => 'FILE' }, @name{qw(o n)});
677 0         0 flush_buffer([], \@old, \@new);
678             }
679             }
680             }
681             #
682             # #ifdef custom container
683             #
684             # #ifdef JA ::::::: ja
685             # japanese text japanese text
686             # #endif :::::::
687             # #ifdef EN ::::::: en
688             # english text english text
689             # #endif :::::::
690             #
691             elsif (/^(\#ifdef|:{7,})\h++(.*)/) {
692             COLON:
693             {
694 0         0 my($c1, $c2, $c3, $c4) = ($_);
  0         0  
695 0         0 my $start = $1;
696 0 0       0 my $end = $start eq '#ifdef' ? qr/^#endif$/ : qr/^$start$/;
697 0         0 my($m1, $m2) = ($2);
698              
699 0     0   0 my @old = read_until { /$end/ } *DIFF;
  0         0  
700 0   0     0 $c2 = pop @old // do {
701 0         0 flush_unknown($c1, @old);
702 0         0 last;
703             };
704 0         0 $c3 = ;
705 0 0       0 if ($c3 !~ /^${start}\h++(.*)/) {
706 0         0 flush_unknown($c1, @old, $c2, $c3);
707 0         0 last;
708             }
709 0         0 $m2 = $1;
710 0     0   0 my @new = read_until { /$end/ } *DIFF;
  0         0  
711 0   0     0 $c4 = pop @new // do {
712 0         0 flush_unknown($c1, @old, $c2, $c3, @new);
713 0         0 last;
714             };
715              
716 0         0 @old = (" $c1", map(s/^/-/r, @old), " $c2");
717 0         0 @new = (" $c3", map(s/^/+/r, @new), " $c4");
718 0         0 flush_buffer([], \@old, \@new);
719             }
720             }
721             else {
722 23         147 flush_unknown($_);
723             }
724             }
725             continue {
726 40         4800821 STDOUT->flush;
727             }
728              
729 4         564 close DIFF;
730 4 50       174 my $exit = $DIFF =~ /\|$/ ? $? >> 8 : 0;
731              
732 4 50       28 if ($readfile) {
733 0 0       0 if ($exit < 2) {
734 0         0 print_identical(-1);
735             }
736 0         0 close OLD;
737 0         0 close NEW;
738             }
739              
740 4         0 exit($exit > 1);
741              
742             ######################################################################
743              
744             ##
745             ## Convert diff -c output to -u compatible format.
746             ##
747             sub merge_diffc {
748 1     1   15 my @o = @{+shift};
  1         12  
749 1         2 my @n = @{+shift};
  1         5  
750 1         2 for (@o, @n) {
751 28 50       86 s/(?<= ^[ \-\+\!] ) [\t ]//x or die "Format error (-c).\n";
752             }
753              
754 1         2 my @buf;
755 1   66     5 while (@o or @n) {
756              
757 6         9 push @buf, \( my( @common, @old, @new ) );
758              
759 6   100     25 while (@o and $o[0] =~ /^ /) {
760 8         9 push @common, shift @o;
761 8 100       18 shift @n if @n;
762             }
763 6   100     17 while (@n and $n[0] =~ /^ /) {
764 3         7 push @common, shift @n;
765             }
766              
767 6   100     24 push @old, shift @o while @o and $o[0] =~ /^\-/;
768 6 100       11 next if @old;
769              
770 5   100     21 push @new, shift @n while @n and $n[0] =~ /^\+/;
771 5 100       8 next if @new;
772              
773 4   100     20 push @old, shift @o while @o and $o[0] =~ s/^!/-/;
774 4   100     35 push @new, shift @n while @n and $n[0] =~ s/^!/+/;
775             }
776              
777 1         4 @buf;
778             }
779              
780             sub flush_unknown {
781 23 50   23   98 if ($app->parallel > 2) {
    50          
782 0         0 flush_buffer_3( [ map s/^/ /r, @_ ] );
783             }
784             elsif ($app->parallel > 1) {
785 0         0 flush_buffer( [ map s/^/ /r, @_ ] );
786             }
787             else {
788 23         265 print $painter->('UNKNOWN', $_) for @_;
789             }
790             }
791              
792             sub flush_buffer {
793              
794 11     11   48 push @_, [] while @_ % 3;
795              
796 11 50       70 if ($app->view) {
797 0         0 @_ = do {
798 0         0 map { @$_ }
799             reduce {
800             [ [] ,
801 0         0 [ map { @$_ } $a->[1], $b->[0], $b->[1] ] ,
802 0     0   0 [ map { @$_ } $a->[2], $b->[0], $b->[2] ] ] }
  0         0  
803 0 0       0 map { $_ ? [ ( splice @_, 0, 3 ) ] : [ [], [], [] ] }
  0         0  
804             0 .. @_ / 3 ;
805             };
806             }
807              
808 11         180 while (my($s, $o, $n) = splice @_, 0, 3) {
809 26         66 for (@$s) {
810 32 50       247 s/^(.)// or die;
811 32         105 print_column_23($1, $_, $1, $_);
812 32         75 $oline++;
813 32         84 $nline++;
814             }
815              
816 26   100     154 while (@$o or @$n) {
817 65         136 my $old = shift @$o;
818 65         125 my $new = shift @$n;
819 65 100 33     407 my $omark = $old ? $old =~ s/^(.)// && $1 : ' ';
820 65 100 33     546 my $nmark = $new ? $new =~ s/^(.)// && $1 : ' ';
821              
822 65         196 print_column_23($omark, $old, $nmark, $new);
823              
824 65 100       183 $oline++ if defined $old;
825 65 100       515 $nline++ if defined $new;
826             }
827             }
828             }
829              
830             sub flush_buffer_3 {
831              
832 0     0   0 push @_, [] while @_ % 4;
833              
834 0 0       0 if ($app->view) {
835 0         0 @_ = do {
836 0         0 map { @$_ }
837             reduce {
838             [ [] ,
839 0         0 [ map { @$_ } $a->[1], $b->[0], $b->[1] ] ,
840 0         0 [ map { @$_ } $a->[2], $b->[0], $b->[2] ] ,
841 0     0   0 [ map { @$_ } $a->[3], $b->[0], $b->[3] ] ] }
  0         0  
842 0 0       0 map { $_ ? [ splice @_, 0, 4 ] : [ [], [], [], [] ] }
  0         0  
843             0 .. @_ / 4;
844             };
845             }
846              
847 0         0 while (@_) {
848 0         0 my @d = splice @_, 0, 4;
849              
850 0         0 for my $common (@{shift @d}) {
  0         0  
851 0         0 $common =~ s/^ //;
852 0         0 print_column_23(' ', $common, ' ', $common, ' ', $common);
853 0         0 $oline++;
854 0         0 $nline++;
855 0         0 $mline++;
856             }
857              
858 0     0   0 while (first { @$_ > 0 } @d) {
  0         0  
859 0         0 my $old = shift @{$d[0]};
  0         0  
860 0         0 my $new = shift @{$d[1]};
  0         0  
861 0         0 my $mrg = shift @{$d[2]};
  0         0  
862 0 0 0     0 my $om = $old ? $old =~ s/^(?|(\-).| (\+)|( ) )// && $1 : ' ';
863 0 0 0     0 my $nm = $new ? $new =~ s/^(?|.(\-)|(\+) |( ) )// && $1 : ' ';
864 0 0 0     0 my $mm = $mrg ? $mrg =~ s/^(?|(\+).|.(\+)|( ) )// && $1 : ' ';
865              
866 0         0 print_column_23($om, $old, $nm, $new, $mm, $mrg);
867              
868 0 0       0 $oline++ if defined $old;
869 0 0       0 $nline++ if defined $new;
870 0 0       0 $mline++ if defined $mrg;
871             }
872             }
873             }
874              
875             sub print_identical {
876 0     0   0 my $n = shift;
877 0         0 while ($n--) {
878 0         0 my $old = ;
879 0         0 my $new = ;
880 0 0 0     0 defined $old or defined $new or last;
881 0         0 print_column_23(' ', $old, ' ', $new);
882 0         0 $oline++;
883 0         0 $nline++;
884 0         0 $mline++;
885             }
886             }
887              
888             sub linenum {
889 0     0   0 my $n = shift;
890 0 0       0 defined $n ? (sprintf $num_format, $n) : (' ' x $app->digit);
891             }
892              
893             sub print_column_23 {
894 97     97   234 my $column = @_ / 2;
895 97         243 my $width = column_width $column;
896 97         377 my($omark, $old, $nmark, $new, $mmark, $mrg) = @_;
897 97         369 my $print_number = $app->number;
898              
899 97         605 my($onum, $nnum, $mnum) = ('', '', '');
900 97 50       219 my $nspace = $print_number ? ' ' : '';
901 97 100       223 if (defined $old) {
902 58         433 $old =~ s/\R\z//;
903 58 50       149 $onum = linenum($oline) if $print_number;
904             }
905 97 100       192 if (defined $new) {
906 85         467 $new =~ s/\R\z//;
907 85 50       218 $nnum = linenum($nline) if $print_number;
908             }
909 97 50       199 if (defined $mrg) {
910 0         0 $mrg =~ s/\R\z//;
911 0 0       0 $mnum = linenum($mline) if $print_number;
912             }
913              
914 97 100       386 my($OTEXT, $OLINE, $OMARK) =
915             $omark =~ /\S/ ? qw(OTEXT OLINE OMARK) : qw(UTEXT OULINE OUMARK);
916 97 100       348 my($NTEXT, $NLINE, $NMARK) =
917             $nmark =~ /\S/ ? qw(NTEXT NLINE NMARK) : qw(UTEXT NULINE NUMARK);
918 97 0       227 my($MTEXT, $MLINE, $MMARK) =
    50          
919             $mmark =~ /\S/ ? qw(MTEXT MLINE MMARK) : qw(UTEXT NULINE NUMARK)
920             if $column >= 3;
921              
922 97         153 while (1) {
923 97         606 (my $o, $old) = ansi_fold($old,
924             max(1, $width - length($onum . $nspace)),
925             @boundary);
926 97         26102 (my $n, $new) = ansi_fold($new,
927             max(1, $width - length($nnum . $nspace)),
928             @boundary);
929 97 50       32964 (my $m, $mrg) = ansi_fold($mrg,
930             max(1, $width - length($mnum . $nspace)),
931             @boundary)
932             if $column >= 3;
933              
934 97         168 my @f;
935 97         284 $f[0]{MARK} = $painter->($OMARK, $omark);
936 97 50       11191 $f[0]{LINE} = $painter->($OLINE, $onum) . $nspace if $print_number;
937 97 50       354 $f[0]{TEXT} = $painter->($OTEXT, $o) if $o ne "";
938 97         8686 $f[1]{MARK} = $painter->($NMARK, $nmark);
939 97 50       6885 $f[1]{LINE} = $painter->($NLINE, $nnum) . $nspace if $print_number;
940 97 50       280 $f[1]{TEXT} = $painter->($NTEXT, $n) if $n ne "";
941 97 50       9915 if ($column >= 3) {
942 0         0 $f[2]{MARK} = $painter->($MMARK, $mmark);
943 0 0       0 $f[2]{LINE} = $painter->($MLINE, $mnum) . $nspace if $print_number;
944 0 0       0 $f[2]{TEXT} = $painter->($MTEXT, $m) if $m ne "";
945             }
946 97         297 print_field_n(@f);
947              
948 97 50       399 last if $app->truncate;
949 97 50 33     1577 last unless $old ne '' or $new ne '' or ($mrg and $mrg ne '');
      33        
      33        
950              
951 0 0       0 if ($print_number) {
952 0         0 $onum =~ s/./ /g;
953 0         0 $nnum =~ s/./ /g;
954 0 0       0 $mnum =~ s/./ /g if $column >= 3;
955             }
956 0 0       0 $omark = $old ne '' ? '.' : ' ';
957 0 0       0 $nmark = $new ne '' ? '.' : ' ';
958 0 0       0 $mmark = $mrg ne '' ? '.' : ' ' if $column >= 3;
    0          
959             }
960             }
961              
962             sub print_command_n {
963 17 100   17   124 my $opt = ref $_[0] ? shift : {};
964 17         42 my $column = @_;
965 17         129 my $width = column_width $column;
966 17         44 my @f;
967              
968 17   100     145 $opt->{type} //= 'COMMAND';
969              
970 17 100 50     125 $app->command or return if $opt->{type} eq 'COMMAND';
971 17 100 50     180 $app->filename or return if $opt->{type} eq 'FILE';
972              
973 17         92 my @color = map { $_ . $opt->{type} } "O", "N", "M";
  51         205  
974              
975 17         86 for my $i (keys @_) {
976 34         120 local $_ = $_[$i];
977 34 50       187 chomp if defined;
978 34         194 ($_) = ansi_fold($_, $width);
979 34         97305 my %f;
980 34 50       181 my $color = $i < @color ? $color[$i] : $color[-1];
981 34         189 $f{TEXT} = $painter->($color, $_);
982 34         15389 $f{MARK} = ' ';
983 34         127 push @f, \%f;
984             }
985              
986 17         125 print_field_n(@f);
987             }
988              
989             sub print_field_n {
990 114 50   114   348 if (@column >= @_) {
991 0         0 @_ = @_[ @column[ keys @_ ] ];
992             }
993 114         515 while (my($i, $f) = each @_) {
994 228 50       686 my $markpos = $i < @markpos ? $markpos[$i] : $markpos[-1];
995 228         370 local $_;
996 228 100 33     689 $_ = $f->{"MARK"} and print if $markpos eq LEFT;
997 228 50       583 $_ = $f->{"LINE"} and print;
998 228 50       1124 $_ = $f->{"TEXT"} and print;
999 228 100 33     1152 $_ = $f->{"MARK"} and print if $markpos eq RIGHT;
1000             }
1001 114         438 print "\n";
1002             }
1003              
1004             __END__