Skip to content

Commit 4936226

Browse files
Add an external implementation (with basic tests) of git ansi color parsing
1 parent e5c5c5d commit 4936226

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
#!/usr/bin/env perl
2+
3+
use strict;
4+
use warnings;
5+
use v5.16;
6+
7+
###############################################################################
8+
###############################################################################
9+
10+
compare_color("\e[1;91m" , git_ansi_color("brightred bold") , "brightred bold");
11+
compare_color("\e[1;31m" , git_ansi_color("red bold") , "red bold");
12+
compare_color("\e[1;32m" , git_ansi_color("green bold") , "green bold");
13+
compare_color("\e[33m" , git_ansi_color("yellow") , "yellow");
14+
compare_color("\e[1;7;32m" , git_ansi_color("green bold reverse"), "green bold reverse");
15+
compare_color("\e[1;35m" , git_ansi_color("magenta bold") , "magenta bold");
16+
compare_color("\e[1;38;5;146m" , git_ansi_color("146 bold") , "146 bold");
17+
compare_color("\e[1;38;5;146;48;5;22m", git_ansi_color("146 bold 22") , "146 bold 22");
18+
compare_color("\e[1;34;40m" , git_ansi_color("blue black bold") , "blue black gold");
19+
compare_color("\e[38;5;11m" , git_ansi_color("11") , "11");
20+
compare_color("\e[7;31m" , git_ansi_color("red reverse") , "red reverse");
21+
compare_color("\e[1;31;48;5;52m" , git_ansi_color("red bold 52") , "red bold 52");
22+
compare_color("\e[38;5;10;48;5;20m" , git_ansi_color("10 20") , "10 20");
23+
24+
###############################################################################
25+
###############################################################################
26+
27+
sub compare_color {
28+
my ($one, $two, $desc) = @_;
29+
30+
if ($one ne $two) {
31+
#k($one, $two);
32+
#printf("No match for %-20s %s / %s\n", $desc, ansi_to_human($one), ansi_to_human($two));
33+
printf("%-20s %sFAIL%s %s ne %s\n", $desc, color('red'), color(), ansi_to_human($one), ansi_to_human($two));
34+
} else {
35+
printf("%-20s %sOK%s\n", $desc, color('green'), color());
36+
}
37+
}
38+
39+
# Convert an ANSI sequence to something printable
40+
sub ansi_to_human {
41+
my $str = shift();
42+
43+
$str =~ s/\e\[/[/g;
44+
$str =~ s/m\b/]/g;
45+
46+
return $str;
47+
}
48+
49+
# Alternate (unused) method to parse git config colors
50+
sub git_ansi_color2 {
51+
my $str = shift();
52+
my @parts = split(' ', $str);
53+
54+
if (!@parts) {
55+
return '';
56+
}
57+
58+
my %map = qw(
59+
black 30 red 31 green 32 yellow 33 blue 34 magenta 35 cyan 36 white 37
60+
bold 1 reverse 7
61+
);
62+
63+
k(\@parts);
64+
65+
my @ret;
66+
my $first = 1;
67+
foreach my $item (@parts) {
68+
my $val = $map{$item};
69+
70+
if (!$val) {
71+
if (!$first && $item > 10) {
72+
$val = $item + 10;
73+
}
74+
push(@ret, (38,5,$val));
75+
} else {
76+
# Background color
77+
if (!$first && $val > 10) {
78+
$val += 10;
79+
$first = 0;
80+
}
81+
push(@ret, $val);
82+
}
83+
84+
if ($val > 10) {
85+
$first = 0;
86+
}
87+
}
88+
89+
my $ret = "\e[" . join(";", @ret) . "m";
90+
91+
print ansi_to_human($ret);
92+
93+
return $ret;
94+
}
95+
96+
# https://www.git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_colors_in_git
97+
sub git_ansi_color {
98+
my $str = shift();
99+
my @parts = split(' ', $str);
100+
101+
if (!@parts) {
102+
return '';
103+
}
104+
my $colors = {
105+
'black' => 0,
106+
'red' => 1,
107+
'green' => 2,
108+
'yellow' => 3,
109+
'blue' => 4,
110+
'magenta' => 5,
111+
'cyan' => 6,
112+
'white' => 7,
113+
};
114+
115+
my @ansi_part = ();
116+
117+
if (grep { /bold/ } @parts) {
118+
push(@ansi_part, "1");
119+
@parts = grep { !/bold/ } @parts; # Remove from array
120+
}
121+
122+
if (grep { /reverse/ } @parts) {
123+
push(@ansi_part, "7");
124+
@parts = grep { !/reverse/ } @parts; # Remove from array
125+
}
126+
127+
my $fg = $parts[0] || "";
128+
my $bg = $parts[1] || "";
129+
130+
#############################################
131+
132+
# It's an numeric value, so it's an 8 bit color
133+
if (is_numeric($fg)) {
134+
push(@ansi_part, "38;5;$fg");
135+
# It's a simple 16 color OG ansi
136+
} elsif ($fg) {
137+
my $bright = $fg =~ s/bright//;
138+
my $color_num = $colors->{$fg} + 30;
139+
140+
if ($bright) { $color_num += 60; } # Set bold
141+
142+
push(@ansi_part, $color_num);
143+
}
144+
145+
#############################################
146+
147+
# It's an numeric value, so it's an 8 bit color
148+
if (is_numeric($bg)) {
149+
push(@ansi_part, "48;5;$bg");
150+
# It's a simple 16 color OG ansi
151+
} elsif ($bg) {
152+
my $bright = $bg =~ s/bright//;
153+
my $color_num = $colors->{$bg} + 40;
154+
155+
if ($bright) { $color_num += 60; } # Set bold
156+
157+
push(@ansi_part, $color_num);
158+
}
159+
160+
#############################################
161+
162+
my $ansi_str = join(";", @ansi_part);
163+
my $ret = "\e[" . $ansi_str . "m";
164+
165+
return $ret;
166+
}
167+
168+
sub is_numeric {
169+
my $s = shift();
170+
171+
if ($s =~ /^\d+$/) {
172+
return 1;
173+
}
174+
175+
return 0;
176+
}
177+
178+
sub trim {
179+
my $s = shift();
180+
if (!defined($s) || length($s) == 0) { return ""; }
181+
$s =~ s/^\s*//;
182+
$s =~ s/\s*$//;
183+
184+
return $s;
185+
}
186+
187+
# String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
188+
sub color {
189+
my $str = shift();
190+
191+
# If we're NOT connected to a an interactive terminal don't do color
192+
#if (-t STDOUT == 0) { return ''; }
193+
194+
# No string sent in, so we just reset
195+
if (!length($str) || $str eq 'reset') { return "\e[0m"; }
196+
197+
# Some predefined colors
198+
my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
199+
$str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;
200+
201+
# Get foreground/background and any commands
202+
my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
203+
my ($bc) = $str =~ /on_(\d{1,3})$/g;
204+
205+
# Some predefined commands
206+
my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
207+
my $cmd_num = $cmd_map{$cmd // 0};
208+
209+
my $ret = '';
210+
if ($cmd_num) { $ret .= "\e[${cmd_num}m"; }
211+
if (defined($fc)) { $ret .= "\e[38;5;${fc}m"; }
212+
if (defined($bc)) { $ret .= "\e[48;5;${bc}m"; }
213+
214+
return $ret;
215+
}
216+
217+
# Debug print variable using either Data::Dump::Color (preferred) or Data::Dumper
218+
# Creates methods k() and kd() to print, and print & die respectively
219+
BEGIN {
220+
if (eval { require Data::Dump::Color }) {
221+
*k = sub { Data::Dump::Color::dd(@_) };
222+
} else {
223+
require Data::Dumper;
224+
*k = sub { print Data::Dumper::Dumper(\@_) };
225+
}
226+
227+
sub kd {
228+
k(@_);
229+
230+
printf("Died at %2\$s line #%3\$s\n",caller());
231+
exit(15);
232+
}
233+
}
234+
235+
# vim: tabstop=4 shiftwidth=4 autoindent softtabstop=4

0 commit comments

Comments
 (0)