File Coverage

blib/lib/App/cat/v.pm
Criterion Covered Total %
statement 106 118 89.8
branch 17 24 70.8
condition 5 11 45.4
subroutine 28 31 90.3
pod 0 5 0.0
total 156 189 82.5


line stmt bran cond sub pod time code
1             package App::cat::v;
2              
3             our $VERSION = "1.05";
4              
5 8     8   262451 use 5.024;
  8         29  
6 8     8   78 use warnings;
  8         17  
  8         565  
7 8     8   5028 use open IO => ':utf8', ':std';
  8         13161  
  8         47  
8              
9 8     8   5642 use utf8;
  8         2278  
  8         51  
10 8     8   4574 use Encode;
  8         160532  
  8         921  
11 8     8   6722 use Data::Dumper;
  8         80919  
  8         843  
12             {
13 8     8   113 no warnings 'redefine';
  8         16  
  8         1248  
14 0     0   0 *Data::Dumper::qquote = sub { qq["${\(shift)}"] };
  0         0  
15             $Data::Dumper::Useperl = 1;
16             }
17 8     8   62 use open IO => 'utf8', ':std';
  8         16  
  8         64  
18 8     8   7299 use charnames ':loose';
  8         94080  
  8         59  
19 8     8   228735 use Pod::Usage;
  8         483563  
  8         1294  
20 8     8   80 use List::Util qw(max pairmap);
  8         14  
  8         1046  
21 8     8   5653 use Hash::Util qw(lock_keys);
  8         31313  
  8         70  
22 8     8   5553 use Getopt::EX;
  8         1161  
  8         346  
23 8     8   4395 use Text::ANSI::Tabs qw(ansi_expand);
  8         228417  
  8         840  
24 8     8   6243 use Term::ANSIColor::Concise qw(ansi_color);
  8         550138  
  8         4770  
25              
26             my %control = (
27             nul => [ 'm', "\x00", { s => "\x{2400}", # ␀ SYMBOL FOR NULL
28             m => "\x{2205}", } ], # ∅ EMPTY SET
29             soh => [ 's', "\x01", { s => "\x{2401}", } ], # ␁ SYMBOL FOR START OF HEADING
30             stx => [ 's', "\x02", { s => "\x{2402}", } ], # ␂ SYMBOL FOR START OF TEXT
31             etx => [ 's', "\x03", { s => "\x{2403}", } ], # ␃ SYMBOL FOR END OF TEXT
32             eot => [ 's', "\x04", { s => "\x{2404}", } ], # ␄ SYMBOL FOR END OF TRANSMISSION
33             enq => [ 's', "\x05", { s => "\x{2405}", } ], # ␅ SYMBOL FOR ENQUIRY
34             ack => [ 's', "\x06", { s => "\x{2406}", } ], # ␆ SYMBOL FOR ACKNOWLEDGE
35             bel => [ 's', "\x07", { s => "\x{2407}", # ␇ SYMBOL FOR BELL
36             m => "\x{237E}", } ], # ⍾ BELL SYMBOL
37             bs => [ 's', "\x08", { s => "\x{2408}", } ], # ␈ SYMBOL FOR BACKSPACE
38             ht => [ 's', "\x09", { s => "\x{2409}", } ], # ␉ SYMBOL FOR HORIZONTAL TABULATION
39             nl => [ 'm', "\x0a", { s => "\x{240A}", # ␊ SYMBOL FOR LINE FEED
40             m => "\x{23CE}", } ], # ⏎ RETURN SYMBOL
41             vt => [ 's', "\x0b", { s => "\x{240B}", } ], # ␋ SYMBOL FOR VERTICAL TABULATION
42             np => [ 'm', "\x0c", { s => "\x{240C}", , # ␌ SYMBOL FOR FORM FEED
43             m => "\x{2398}", } ], # ⎘ NEXT PAGE
44             cr => [ 's', "\x0d", { s => "\x{240D}", } ], # ␍ SYMBOL FOR CARRIAGE RETURN
45             so => [ 's', "\x0e", { s => "\x{240E}", } ], # ␎ SYMBOL FOR SHIFT OUT
46             si => [ 's', "\x0f", { s => "\x{240F}", } ], # ␏ SYMBOL FOR SHIFT IN
47             dle => [ 's', "\x10", { s => "\x{2410}", } ], # ␐ SYMBOL FOR DATA LINK ESCAPE
48             dc1 => [ 's', "\x11", { s => "\x{2411}", } ], # ␑ SYMBOL FOR DEVICE CONTROL ONE
49             dc2 => [ 's', "\x12", { s => "\x{2412}", } ], # ␒ SYMBOL FOR DEVICE CONTROL TWO
50             dc3 => [ 's', "\x13", { s => "\x{2413}", } ], # ␓ SYMBOL FOR DEVICE CONTROL THREE
51             dc4 => [ 's', "\x14", { s => "\x{2414}", } ], # ␔ SYMBOL FOR DEVICE CONTROL FOUR
52             nak => [ 's', "\x15", { s => "\x{2415}", } ], # ␕ SYMBOL FOR NEGATIVE ACKNOWLEDGE
53             syn => [ 's', "\x16", { s => "\x{2416}", } ], # ␖ SYMBOL FOR SYNCHRONOUS IDLE
54             etb => [ 's', "\x17", { s => "\x{2417}", } ], # ␗ SYMBOL FOR END OF TRANSMISSION BLOCK
55             can => [ 's', "\x18", { s => "\x{2418}", } ], # ␘ SYMBOL FOR CANCEL
56             em => [ 's', "\x19", { s => "\x{2419}", } ], # ␙ SYMBOL FOR END OF MEDIUM
57             sub => [ 's', "\x1a", { s => "\x{241A}", } ], # ␚ SYMBOL FOR SUBSTITUTE
58             esc => [ '0', "\x1b", { s => "\x{241B}", # ␛ SYMBOL FOR ESCAPE
59             m => "\x{21B0}", } ], # ↰ UPWARDS ARROW WITH TIP LEFTWARDS
60             fs => [ 's', "\x1c", { s => "\x{241C}", } ], # ␜ SYMBOL FOR FILE SEPARATOR
61             gs => [ 's', "\x1d", { s => "\x{241D}", } ], # ␝ SYMBOL FOR GROUP SEPARATOR
62             rs => [ 's', "\x1e", { s => "\x{241E}", } ], # ␞ SYMBOL FOR RECORD SEPARATOR
63             us => [ 's', "\x1f", { s => "\x{241F}", } ], # ␟ SYMBOL FOR UNIT SEPARATOR
64             sp => [ 'm', "\x20", { s => "\x{2420}", # ␠ SYMBOL FOR SPACE
65             m => "\x{00B7}", } ], # · MIDDLE DOT
66             del => [ 'm', "\x7f", { s => "\x{2421}", , # ␡ SYMBOL FOR DELETE
67             m => "\x{232B}", } ], # ⌫ ERASE TO THE LEFT
68             nbsp => [ 's', "\xa0", { s => "\x{237D}", } ], # ⍽ SHOULDERED OPEN BOX
69             );
70              
71             package #
72             Visibility {
73 8     8   140 use v5.24;
  8         30  
74 8     8   43 use warnings;
  8         12  
  8         6165  
75 282     282   832 sub default { $_[0]->[0] }
76 632     632   1112 sub code { $_[0]->[1] }
77 353     353   1087 sub cmap { $_[0]->[2] }
78             sub visible {
79 71     71   152 my($c, $type) = @_;
80 71   50     170 $c->cmap->{$type} // $c->cmap->{$c->default || 's'};
      66        
81             }
82             };
83             bless $_, 'Visibility' for values %control;
84              
85             # setup 'e' map
86             for my $v (values %control) {
87             my %map = (
88             "\t" => '\t',
89             "\n" => '\n',
90             "\r" => '\r',
91             "\f" => '\f',
92             "\b" => '\b',
93             "\a" => '\a',
94             "\e" => '\e',
95             );
96             my $code = $v->code;
97             $v->cmap->{e} = $map{$code} // sprintf "\\x%02x", ord($code);
98             }
99              
100             my %code = pairmap { $a => $b->code } %control;
101              
102             our $DEFAULT_TABSTYLE = 'needle';
103             if ($DEFAULT_TABSTYLE) {
104             Text::ANSI::Tabs->configure(tabstyle => $DEFAULT_TABSTYLE);
105             }
106              
107             sub list_tabstyle {
108 0     0 0 0 my %style = %Text::ANSI::Fold::TABSTYLE;
109 0         0 my $max = max map length, keys %style;
110 0         0 for my $name (sort keys %style) {
111 0         0 my($head, $space) = $style{$name}->@*;
112 0         0 my $tab = $head . $space x 7;
113 0         0 printf "%*s %s\n", $max, $name, $tab x 3;
114             }
115             }
116              
117 8     8   5296 use Getopt::EX::Hashed; {
  8         39511  
  8         59  
118             Getopt::EX::Hashed->configure(DEFAULT => [ is => 'rw' ]);
119             has visible => ' c =s@ ' ;
120             has reset => ' n ' ;
121             has color => ' C =s ' ;
122             has expand => ' t :1 ' , default => 1 ;
123             has no_expand => ' T ! ' ;
124             has repeat => ' r =s ' , default => 'nl,np' ;
125             has original => ' o + ' , default => 0 ;
126             has debug => ' d ' ;
127             has tabstop => ' x =i ' , default => 8, min => 1 ;
128             has tabhead => ' =s ' ;
129             has tabspace => ' =s ' ;
130             has tabstyle => ' ts :s ' , default => $DEFAULT_TABSTYLE ;
131             has help => ' h ' ;
132             has version => ' v ' ;
133             has escape_backslash => 'E!' ;
134              
135             # -n
136             has '+reset' => sub {
137             for my $name (keys $_->flags->%*) {
138             $_->flags->{$name} = '0';
139             }
140             $_->repeat = '';
141             $_->expand = 0;
142             };
143              
144             has '+expand' => sub {
145             $_->expand = $_[1];
146             if ($_[1] > 1) {
147             $_->tabstop = $_[1];
148             Text::ANSI::Tabs->configure(tabstop => $_->tabstop);
149             }
150             };
151              
152             # -T negate -t
153             has '+no_expand' => sub {
154             $_->expand = ! $_[1];
155             };
156              
157             has '+repeat' => sub {
158             my($name, $c) = ("$_[0]", $_[1]);
159             if ($c =~ s/^\++//) {
160             $_->$name .= ",$c";
161             } else {
162             $_->$name = $c;
163             }
164             };
165              
166             # individual char option
167             has [ keys %control ] => ':s', action => sub {
168             my($name, $c) = ("$_[0]", $_[1]);
169             if ($c =~ s/^\+//) {
170             $_->repeat .= ",$name";
171             }
172             $c = '1' if $c eq '';
173             if (length($c) > 1 and my $u = charnames::string_vianame($c)) {
174             $c = $u;
175             }
176             $_->flags->{$name} = $c;
177             };
178              
179             ### --tabstop, --tabstyle
180             has [ qw(+tabstop +tabstyle) ] => sub {
181             my($name, $val) = map "$_", @_;
182             if ($val eq '') {
183             list_tabstyle();
184             exit;
185             }
186             Text::ANSI::Tabs->configure($name => $val);
187             };
188              
189             ### --tabhead, --tabspace
190             has [ qw(+tabhead +tabspace) ] => sub {
191             my($name, $c) = map "$_", @_;
192             $c = charnames::string_vianame($c) || die "$c: invalid name\n"
193             if length($c) > 1;
194             Text::ANSI::Tabs->configure($name => $c);
195             };
196              
197             has '+visible' => sub {
198             my $param = $_[1];
199             if ($param !~ /^\w+=/) {
200             $param = "all=$param";
201             }
202             $param =~ s{ \ball\b }{ join('=', keys $_->flags->%*) }xe;
203             push @{$_->visible}, $param;
204             };
205              
206             has '+help' => sub {
207             pod2usage
208             -verbose => 99,
209             -sections => [ qw(SYNOPSIS VERSION) ];
210             };
211              
212             has '+version' => sub {
213             say "Version: $VERSION";
214             exit;
215             };
216              
217             # internal use
218              
219             has flags => default => { pairmap { $a => $b->default } %control };
220             has convert => default => {};
221             has colorize => default => sub { sub { $_[0] } } ;
222              
223 8     8   15451 } no Getopt::EX::Hashed;
  8         14  
  8         40  
224              
225             sub run {
226 7     7 0 287 my $app = shift;
227 7         36 local @ARGV = splice @_;
228 7         40 $app->options->setup->doit;
229 7         124 return 0;
230             }
231              
232             sub options {
233 7     7 0 16 my $app = shift;
234 7         21 for (@ARGV) {
235 16 50       397 $_ = decode 'utf8', $_ unless utf8::is_utf8($_);
236             }
237 8     8   7003 use Getopt::EX::Long qw(:DEFAULT ExConfigure Configure);
  8         312006  
  8         11775  
238 7         225 ExConfigure BASECLASS => [ __PACKAGE__, 'Getopt::EX' ];
239 7         338 Configure qw(bundling);
240 7 50       483 $app->getopt || pod2usage();
241              
242 7         23592 Getopt::EX::LabeledParam
243             ->new(HASH => $app->flags, NEWLABEL => 0, DEFAULT => 1)
244             ->load_params($app->visible->@*);
245              
246 7 50       631 if (my $color = $app->color) {
247 0     0   0 $app->colorize = sub { ansi_color($color, @_) };
  0         0  
248             }
249              
250 7         77 return $app;
251             }
252              
253             sub setup {
254 7     7 0 18 my $app = shift;
255 7         39 my $convert = $app->convert;
256 7         43 my $flags = $app->flags;
257 7         75 for my $name (keys $flags->%*) {
258 245 100       524 my $flag = $flags->{$name} or next;
259 72         140 my $char = $control{$name};
260 72         178 my $code = $char->code;
261 72 100       253 if ($flag eq 'c') {
    50          
262 1 50       4 if ($code =~ /[\x00-\x1f]/) {
263 1         7 $convert->{$code} = '^' . pack('c',ord($code)+64);
264             }
265             }
266             elsif ($flag =~ /^([a-z\d])$/i) {
267 71         164 $convert->{$code} = $char->visible($flag);
268             }
269             else {
270 0         0 $convert->{$char->code} = $flag;
271             }
272             }
273 7 50       47 $convert->{"\\"} = "\\\\" if $app->escape_backslash;
274 7         78 return $app;
275             }
276              
277             sub doit {
278 7     7 0 15 my $app = shift;
279 7         23 my $convert = $app->convert;
280 7         90 my $replace = join '', sort keys $convert->%*;
281 7         17 my $repeat_re = do {
282 7 100       30 if (my @c = map { $code{$_} } $app->repeat =~ /\w+/g) {
  7         104  
283 5         14 local $" = "";
284 5         167 qr/[\Q@c\E]/;
285             } else {
286 2         15 qr/(*F)/;
287             }
288             };
289 7         928 while (<>) {
290 70         151 my $orig = $_;
291 70 100       221 $_ = ansi_expand($_) if $app->expand;
292 70 100       49567 if ($replace ne '') {
293 60         1502 s{
294             (?= (? ${repeat_re}?) )
295             (? [\Q$replace\E]
296             | (*F) # without this line, the regex does not work
297             )
298             }{
299             $app->colorize->($convert->{$+{c}}) . $+{r}
300 1494         6514 }xeg;
301             }
302 70 50 33     1124 if ($app->original > 1 or ($app->original and $_ ne $orig)) {
      33        
303 0         0 print $orig;
304             }
305 70         1572 print;
306             }
307             }
308              
309             1;
310              
311             __END__