App-ccdiff
view release on metacpan or search on metacpan
lib/App/ccdiff.pm view on Meta::CPAN
chr_new => "\x{25b2}",
chr_old => "\x{25bc}",
ellipsis => 0,
emacs => 0,
header => 1, # A color name is allowed
heuristics => 0,
index => 0,
iwbzusepp => 0,
new_label => undef,
old_label => undef,
markers => 0,
new => "green",
old => "red",
reverse => 0,
swap => 0,
threshold => 2,
utf8 => 0,
verbose => "cyan",
);
read_rc ();
my $opt_a = $rc{ascii};
my $opt_b;
my $opt_B;
#y $opt_c;
my $opt_E;
my $opt_h = $rc{heuristics};
my $opt_H = $rc{header};
my $opt_i;
my $opt_I = $rc{index};
my $opt_m = $rc{markers};
my $opt_r = $rc{reverse};
my $opt_s = $rc{swap};
my $opt_t = $rc{threshold};
my $opt_e = $rc{ellipsis};
my $opt_u = $rc{unified};
my $opt_U = $rc{utf8};
my $opt_v = 0;
my $opt_w;
my $opt_Z;
my $emacs = $rc{emacs};
my $old_color = $rc{old};
my $new_color = $rc{new};
my $rev_color = $rc{bg};
my $cli_color = $ENV{CLICOLOR}; # https://bixense.com/clicolors/
my $no_colors = $ENV{NO_COLOR}; # https://no-color.org
my $old_label = $rc{old_label};
my $new_label = $rc{new_label};
my $list_colors;
my $diff_class;
if ($no_colors) {
$ENV{CLICOLOR_FORCE} and $no_colors = 0;
}
elsif (defined $cli_color) {
# true $cli_color is the default for ccdiff
!$cli_color || !-t and $no_colors = 1;
}
unless (caller) {
$ENV{CCDIFF_OPTIONS} and unshift @ARGV, split m/\s+/ => $ENV{CCDIFF_OPTIONS};
GetOptions (
"help|?" => sub { usage (0); },
"V|version" => sub { say "$CMD [$VERSION]"; exit 0; },
"man" => sub { pod_nroff (); },
"info" => sub { pod_text (); },
"U|utf-8!" => \$opt_U,
"dc|diff-class=s" => \$diff_class,
"pp!" => sub { $diff_class = "PP" },
# "c|context:3" => \$opt_c, # implement context-diff?
"u|unified:3" => \$opt_u,
"I|idx|index:-1" => \$opt_I,
"t|threshold=i" => \$opt_t,
"H|header!" => \$opt_H,
"HC|header-color=s" => \$opt_H,
"h|heuristics=i" => \$opt_h,
"e|ellipsis=i" => \$opt_e,
"emacs!" => \$emacs,
"a|ascii" => sub { $opt_a ^= 1 },
"m|markers" => sub { $opt_m ^= 1 },
"r|reverse|invert" => sub { $opt_r ^= 1 },
"s|swap!" => sub { $opt_s ^= 1 },
"i|ignore-case!" => \$opt_i,
"w|ignore-all-space!" => \$opt_w,
"b|ignore-ws|ignore-space-change!" => \$opt_b,
"Z|ignore-trailing-space!" => \$opt_Z,
"E|ignore-tab-expansion!" => \$opt_E, # NYI
"B|ignore-blank-lines!" => \$opt_B, # Partly implemented
"old-label|label-old=s" => \$old_label,
"new-label|label-new=s" => \$new_label,
"p|pink!" => sub { $old_color = "magenta" },
"old=s" => \$old_color,
"new=s" => \$new_color,
"bg=s" => \$rev_color,
"no-colors" => \$no_colors,
"list-colors!" => \$list_colors,
"settings|defaults" => sub {
binmode STDOUT, ":encoding(utf-8)";
printf "%-10s : %s\n", $_, $rc{$_} // "<undef>" for sort keys %rc;
exit 0;
},
"v|verbose:1" => \$opt_v,
) or usage (1);
}
$opt_w and $opt_b = $opt_Z = $opt_E = $opt_B = 1;
$opt_h >= 1 and $opt_h /= 100;
sub pod_text {
require Pod::Text::Color;
my $m = $no_colors ? "Pod::Text" : "Pod::Text::Color";
my $p = $m->new ();
open my $fh, ">", \my $out;
$p->parse_from_file ($0, $fh);
close $fh;
print $out;
exit 0;
} # pod_text
sub pod_nroff {
first { -x "$_/nroff" } grep { -d } split m/:+/ => $ENV{PATH} or pod_text ();
require Pod::Man;
my $p = Pod::Man->new ();
open my $fh, "|-", "nroff", "-man";
$p->parse_from_file ($0, $fh);
close $fh;
exit 0;
} # pod_nroff
# Color initialization
for ($old_color, $new_color, $rev_color) {
s/^(.*)[ _]bold$/bold $1/i;
s/^bold_/bold /i;
}
my %clr = map { $_ => color (s{^(.*)[ _]bold$}{bold $1}ir =~
s{^bold[ _]}{bold }ir) }
map {( $_, "on_$_", "bold $_" )}
qw( red green blue black white cyan magenta yellow );
$clr{$_} //= color ($_) for tac_colors ();
$no_colors and $clr{$_} = "" for keys %clr;
$clr{none} = $clr{on_none} = "";
my ($reset, $bg_new, $bg_old,
$chr_cml, $chr_cmr, $chr_ctx, $chr_eli, $chr_eql, $chr_lft,
$chr_new, $chr_old, $chr_rgt,
$clr_dbg, $clr_grn, $clr_new, $clr_old, $clr_red, $clr_rev,
$cmp_sub) = (RESET);
if ($list_colors) {
my @clr = map { sprintf "%s%-18s%s", $clr{$_}, $_, $reset } sort keys %clr;
while (@clr) {
say join " " => map { $_ // "" } splice @clr, 0, 4;
}
exit;
}
sub set_options {
for ([ \$old_color, $rc{old} ], [ \$new_color, $rc{new} ], [ \$rev_color, $rc{bg} ]) {
my ($c, $def) = @$_;
$$c && exists $clr{$$c} and next;
warn "color ", $$c // "(undefined)", " is unknown, using $def instead\n";
$$c = $def;
}
$clr_red = $clr{$old_color};
$clr_grn = $clr{$new_color};
$clr_rev = $clr{$rev_color};
$clr_dbg = $opt_r && exists $clr{"on_$rc{verbose}"} ? $clr{"on_$rc{verbose}"} : $clr{$rc{verbose}};
$reset = $no_colors ? "" : RESET;
$bg_old = $clr{$rc{bg_old} || ($opt_r ? "on_$old_color" =~ s/bold //ir :
"on_$rev_color" =~ s/bold //ir)};
$bg_new = $clr{$rc{bg_new} || ($opt_r ? "on_$new_color" =~ s/bold //ir :
"on_$rev_color" =~ s/bold //ir)};
$clr_old = $opt_r ? $clr_rev . $bg_old : $clr_red . $bg_old;
$clr_new = $opt_r ? $clr_rev . $bg_new : $clr_grn . $bg_new;
$opt_s and ($clr_new, $clr_old) = ($clr_old, $clr_new);
# Indicators
if ($opt_a) {
@rc{qw( chr_old chr_new chr_cml chr_cmr chr_eli chr_eli_v )} = qw( ^ ^ > < - <> );
}
lib/App/ccdiff.pm view on Meta::CPAN
$o eq "ascii" and $opt_a = $v;
$o eq "bg" and $rev_color = $v;
# $o eq "context" and $opt_c = $v;
$o eq "ellipsis" and $opt_e = $v;
$o eq "emacs" and $emacs = $v;
$o eq "header" and $opt_H = $v;
$o eq "heuristics" and $opt_h = $v;
$o eq "ignore-all-space" and $opt_w = $v;
$o eq "ignore-blank-lines" and $opt_B = $v;
$o eq "ignore-case" and $opt_i = $v;
$o eq "ignore-space-change" and $opt_b = $v;
$o eq "ignore-tab-expansion" and $opt_E = $v;
$o eq "ignore-trailing-space" and $opt_Z = $v;
$o eq "index" and $opt_I = $v;
$o eq "list-colors" and $list_colors = $v;
$o eq "markers" and $opt_m = $v;
$o eq "new" and $new_color = $v;
$o eq "old" and $old_color = $v;
$o eq "new-label" and $new_label = $v;
$o eq "old-label" and $old_label = $v;
$o eq "reverse" and $opt_r = $v;
$o eq "swap" and $opt_s = $v;
$o eq "threshold" and $opt_t = $v;
$o eq "unified" and $opt_u = $v;
$o eq "unified" and $opt_u = $v;
$o eq "utf-8" and $opt_U = $v;
$o eq "verbose" and $opt_v = $v;
if ($o eq "out") {
open $fh, ">", $v or die "Cannot select out: $!\n";
select $fh;
}
}
}
set_options ();
$emacs and @_ == 0 && -f $f1 && -f "$f1~" and ($f1, $f2) = ("$f1~", $f1);
$f1 eq "-" && $f2 eq "-" and usage (1);
binmode STDERR, ":encoding(utf-8)";
if ($opt_U) {
binmode STDIN, ":encoding(utf-8)";
binmode STDOUT, ":encoding(utf-8)";
}
my @d1 = ref $f1 eq "ARRAY" ? @$f1 : $f1 eq "-" ? <STDIN> : do {
open my $fh, "<", $f1 or die "$f1: $!\n";
$opt_U and binmode $fh, ":encoding(utf-8)";
<$fh>;
};
my @d2 = ref $f2 eq "ARRAY" ? @$f2 : $f2 eq "-" ? <STDIN> : do {
open my $fh, "<", $f2 or die "$f2: $!\n";
$opt_U and binmode $fh, ":encoding(utf-8)";
<$fh>;
};
if ($opt_H) {
my $hc = "";
if ($opt_H =~ m/^\w\w+/) {
my ($hfg, $hbg) = split m/_?(?=on_)/ => lc $opt_H =~ s/\s+/_/gr;
$hfg && defined $clr{$hfg} and $hc .= $clr{$hfg};
$hbg && defined $clr{$hbg} and $hc .= $clr{$hbg};
}
my $nl = max length $f1, length $f2, 7;
my $sl = $hc ? ($ENV{COLUMNS} || 80) - 4 - $nl : 1;
my $hi = -1;
my @hi = ($old_label, $new_label);
my @h = map { $hi++; -f $_
? { tag => "",
name => $_,
stamp => scalar localtime ((stat $_)[9]),
}
: { tag => "",
name => $hi[$hi] // "*STDIN",
stamp => scalar localtime,
}
} $f1, $f2;
if (defined $opt_u) {
($h[0]{tag}, $h[1]{tag}) = ("---", "+++");
$sl -= 2;
printf "%s%s %-*s %-*s%s\n", $hc, $_->{tag},
$nl, $_->{name}, $sl, $_->{stamp}, $clr{reset} for @h;
}
#elsif ($opt_c) { # diff -c also provides (ugly) headers, but opt_c is NYI
# }
else {
($h[0]{tag}, $h[1]{tag}) = ("<", ">");
printf "%s%s %-*s %-*s%s\n", $hc, $_->{tag},
$nl, $_->{name}, $sl, $_->{stamp}, $clr{reset} for @h;
}
}
my $diff = $diff_class->new (\@d1, \@d2, $cmp_sub);
$diff->Base (1);
my ($N, $idx, @s) = (0, 0);
while ($diff->Next) {
$N++;
if ($diff->Same) {
if (defined $opt_u) {
@s = $diff->Items (1);
$N > 1 and print "$chr_ctx$_" for grep { defined } @s[0..($opt_u - 1)];
unshift @s, undef while @s < $opt_u;
}
next;
}
my $sep = "";
my @d = map {[ $diff->Items ($_) ]} 1, 2;
my @do = @{$d[0]};
my @dn = @{$d[1]};
if ($opt_B and "@do" !~ m/\S/ && "@dn" !~ m/\S/) {
# Modify @s for -u?
next;
}
if ($opt_I) {
$idx++;
$opt_I > 0 && $idx != $opt_I and next;
printf "%s[%03d]%s ", ${clr_dbg}, $idx, $reset;
}
if (!@dn) {
printf "%d,%dd%d\n", $diff->Get (qw( Min1 Max1 Max2 ));
$_ = $clr_old . (s/$/$reset/r) for @do;
}
elsif (!@do) {
printf "%da%d,%d\n", $diff->Get (qw( Max1 Min2 Max2 ));
$_ = $clr_new . (s/$/$reset/r) for @dn;
}
else {
$sep = "---\n" unless defined $opt_u;
printf "%d,%dc%d,%d\n", $diff->Get (qw( Min1 Max1 Min2 Max2 ));
if ($opt_t > 0 and abs (@do - @dn) > $opt_t) {
$_ = $clr_old . (s/$/$reset/r) for @do;
$_ = $clr_new . (s/$/$reset/r) for @dn;
}
else {
my @D = subdiff (@d, my $heu = {});
if ($opt_h and $heu->{pct} > $opt_h) {
$_ = $clr_old . (s/$/$reset/r) for @do;
$_ = $clr_new . (s/$/$reset/r) for @dn;
}
else {
@do = @{$D[0]};
@dn = @{$D[1]};
}
}
}
if ($opt_u and @s) {
print "$chr_ctx$_" for grep { defined } map { $s[$#s - $opt_u + $_] } 1..$opt_u;
}
print "$chr_lft$_" for @do;
print $sep;
print "$chr_rgt$_" for @dn;
}
if ($fh) {
select STDOUT;
close $fh;
}
} # ccdiff
sub subdiff {
my ($old, $new, $heu) = @_;
my $d = $diff_class->new (map { [ map { split m// } @$_ ] } $old, $new);
my ($d1, $d2, $x1, $x2, @h1, @h2) = ("", "", "", "");
my ($cml, $cmr) = $opt_v < 2 ? ("", "") : ($chr_cml, $chr_cmr);
my ($cmd, $cma) = ($chr_old, $chr_new);
@{$heu}{qw( old new same )} = (1, 1, 1); # prevent div/0
while ($d->Next) {
my @c = map {[ $d->Items ($_) ]} 1, 2;
my @co = @{$c[0]};
my @cn = @{$c[1]};
if ($d->Same) {
$heu->{same} += scalar @co;
my $e = $chr_eli;
my $c = join "" => @co;
if ($opt_e) {
my $join = "";
foreach my $sc (split m/\n/ => $c) {
$_ .= $join for $d1, $d2, $x1, $x2;
$join = "\n";
my $l = length $sc; # The length of this "same" chunck
my $le = $l - 2 * $opt_e; # The length of the text replaces with ellipsis
my $ee = $opt_v <= 1 ? $e : $e =~ s/^.\K(?=.$)/$le/r;
if ($le > length $ee) {
my $lsc = substr $sc, 0, $opt_e;
$d1 .= $lsc;
$d2 .= $lsc;
$lsc =~ s/\S/$chr_eql/g;
$x1 .= $lsc;
$x2 .= $lsc;
my $rsc = substr $sc, $l - $opt_e, $opt_e;
$d1 .= $clr_dbg . $ee . $reset . $rsc;
$d2 .= $clr_dbg . $ee . $reset . $rsc;
$rsc =~ s/\S/$chr_eql/g;
$x1 .= $chr_eql x length ($ee) . $rsc;
$x2 .= $chr_eql x length ($ee) . $rsc;
next;
}
else {
$d1 .= $sc;
$d2 .= $sc;
$sc =~ s/\S/$chr_eql/g;
$x1 .= $sc;
$x2 .= $sc;
}
}
next;
}
$d1 .= $c;
$d2 .= $c;
$c =~ s/\S/$chr_eql/g;
$x1 .= $c;
$x2 .= $c;
next;
}
if (@co) {
$heu->{old} += scalar @co;
$d1 .= $cml.$clr_old;
$d1 .= s/\n/$reset\n$clr_old/gr for @co;
$d1 .= $reset.$cmr;
$x1 .= $_ for map { s/[^\t\r\n]/$cmd/gr } @co;
$opt_v and push @h1, map { $opt_U ? charnames::viacode (ord) : unpack "H*"; } @co;
}
if (@cn) {
$heu->{new} += scalar @cn;
$d2 .= $cml.$clr_new;
$d2 .= s/\n/$reset\n$clr_new/gr for @cn;
$d2 .= $reset.$cmr;
$x2 .= $_ for map { s/[^\t\r\n]/$cma/gr } @cn;
$opt_v and push @h2, map { $opt_U ? charnames::viacode (ord) : unpack "H*"; } @cn;
}
}
$heu->{pct} = ($heu->{old} + $heu->{new}) / (2 * $heu->{same});
my @d = map { [ split m/(?<=\n)/ => s/\n*\z/\n/r ] } $d1, $d2;
if ($opt_m) {
$opt_v > 1 and s/(\S+)/ $1 /g for $x1, $x2;
s/[ \t]*\n*\z/\n/ for $x1, $x2;
my @x = map { /\S/ ? [ split m/(?<=\n)/ ] : [] } $x1, $x2;
foreach my $n (0, 1) {
@{$x[$n]} and $d[$n] = [ map {( $d[$n][$_], $x[$n][$_] // "" )} 0 .. (scalar @{$d[$n]} - 1) ];
}
}
if ($opt_v) {
$opt_U && $opt_v > 2 and $_ .= sprintf " (U+%06X)", charnames::vianame ($_) for @h1, @h2;
@h1 and push @{$d[0]}, sprintf " -- ${clr_dbg}verbose$reset : %s\n", join ", " => map { $clr_old.$_.$reset } @h1;
@h2 and push @{$d[1]}, sprintf " -- ${clr_dbg}verbose$reset : %s\n", join ", " => map { $clr_new.$_.$reset } @h2;
}
@d;
} # subdiff
sub read_rc {
my $home = $ENV{HOME} || $ENV{USERPROFILE} || $ENV{HOMEPATH};
foreach my $rcf (
"$home/ccdiff.rc",
"$home/.ccdiffrc",
"$home/.config/ccdiff",
) {
-s $rcf or next;
(stat $rcf)[2] & 022 and next;
open my $fh, "<", $rcf or next;
while (<$fh>) {
my ($k, $v) = (m/^\s*([-\w]+)\s*[:=]\s*(.*\S)/) or next;
$rc{ lc $k
=~ s{[-_]colou?r$}{}ir
=~ s{background}{bg}ir
=~ s{^(?:unicode|utf-?8?)$}{utf8}ir
} = $v
=~ s{U\+?([0-9A-Fa-f]{2,7})}{chr hex $1}ger
=~ s{^(?:no|false)$}{0}ir
=~ s{^(?:yes|true)$}{-1}ir; # -1 is still true
}
}
} # read_rc
# Return the known colors from Term::ANSIColor
# Stolen straight from the pm
sub tac_colors {
my %c256;
foreach my $r (0 .. 5) {
foreach my $g (0 .. 5) {
$c256{lc $_}++ for map {("RGB$r$g$_", "ON_RGB$r$g$_")} 0 .. 5;
}
}
$c256{lc $_}++ for
# Basic colors
qw(
CLEAR RESET BOLD DARK
FAINT ITALIC UNDERLINE UNDERSCORE
BLINK REVERSE CONCEALED
BLACK RED GREEN YELLOW
BLUE MAGENTA CYAN WHITE
ON_BLACK ON_RED ON_GREEN ON_YELLOW
ON_BLUE ON_MAGENTA ON_CYAN ON_WHITE
BRIGHT_BLACK BRIGHT_RED BRIGHT_GREEN BRIGHT_YELLOW
BRIGHT_BLUE BRIGHT_MAGENTA BRIGHT_CYAN BRIGHT_WHITE
ON_BRIGHT_BLACK ON_BRIGHT_RED ON_BRIGHT_GREEN ON_BRIGHT_YELLOW
( run in 0.714 second using v1.01-cache-2.11-cpan-f5b5a18a01a )