prepare to fix spark distribution
[barcat.git] / barcat
1 #!/usr/bin/perl -CA
2 use 5.014;
3 use warnings;
4 use utf8;
5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
7 use re '/msx';
8
9 our $VERSION = '1.08';
10
11 my %opt;
12 if (@ARGV) {
13 require Getopt::Long;
14 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
15 GetOptions(\%opt,
16         'ascii|a!',
17         'color|c!',
18         'C' => sub { $opt{color} = 0 },
19         'field|f=s' => sub {
20                 eval {
21                         local $_ = $_[1];
22                         $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
23                 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
24         },
25         'human-readable|H!',
26         'interval|t:i',
27         'trim|length|l=s' => sub {
28                 my ($optname, $optval) = @_;
29                 $optval =~ s/%$// and $opt{trimpct}++;
30                 $optval =~ m/\A-?[0-9]+\z/ or die(
31                         "Value \"$optval\" invalid for option $optname",
32                         " (number or percentage expected)\n"
33                 );
34                 $opt{trim} = $optval;
35         },
36         'value-length=i',
37         'hidemin=i',
38         'hidemax=i',
39         'minval=f',
40         'maxval=f',
41         'limit|L:s' => sub {
42                 my ($optname, $optval) = @_;
43                 $optval ||= 0;
44                 $optval =~ /\A-[0-9]+\z/ and $optval .= '-';  # tail shorthand
45                 ($opt{hidemin}, $opt{hidemax}) =
46                 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
47                         "Value \"$optval\" invalid for option limit",
48                         " (range expected)\n"
49                 );
50         },
51         'header!',
52         'markers|m=s',
53         'graph-format=s' => sub {
54                 $opt{'graph-format'} = substr $_[1], 0, 1;
55         },
56         'spark:s' => sub {
57                 $opt{spark} = [split //,
58                         $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
59                 ];
60         },
61         'palette=s' => sub {
62                 $opt{palette} = {
63                         fire   => [qw( 90 31 91 33 93 97 96 )],
64                         fire88 => [map {"38;5;$_"} qw(
65                                 80  32 48 64  68 72 76  77 78 79  47
66                         )],
67                         fire256=> [map {"38;5;$_"} qw(
68                                 235  52 88 124 160 196
69                                 202 208 214 220 226  227 228 229 230 231  159
70                         )],
71                         ramp88 => [map {"38;5;$_"} qw(
72                                 64 65 66 67 51 35 39 23 22 26 25 28
73                         )],
74                         whites => [qw( 1;30 0;37 1;37 )],
75                         greys  => [map {"38;5;$_"} 52, 235..255, 47],
76                 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
77         },
78         'stat|s!',
79         'signal-stat=s',
80         'unmodified|u!',
81         'width|w=i',
82         'version|V' => sub {
83                 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
84                 say "barcat $mascot version $VERSION";
85                 exit;
86         },
87         'usage|h' => sub {
88                 /^=/ ? last : print for readline *DATA;  # text between __END__ and pod
89                 exit;
90         },
91         'help|?'  => sub {
92                 require Pod::Usage;
93                 Pod::Usage::pod2usage(
94                         -exitval => 0, -perldocopt => '-oman', -verbose => 2,
95                 );
96         },
97 ) or exit 64;  # EX_USAGE
98 }
99
100 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
101 $opt{color} //= -t *STDOUT;  # enable on tty
102 $opt{'graph-format'} //= '-';
103 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
104 $opt{units}   = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
105         if $opt{'human-readable'};
106 $opt{anchor} //= qr/\A/;
107 $opt{'value-length'} = 6 if $opt{units};
108 $opt{'value-length'} = 1 if $opt{unmodified};
109 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
110 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
111 $opt{palette} //= $opt{color} && [31, 90, 32];
112 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
113 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
114         and undef $opt{interval};
115
116 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
117 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
118 $opt{'value-format'} = $opt{units} && sub {
119         my $unit = (
120                 log(abs $_[0] || 1) / log(10)
121                 - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
122                 + 1e-15  # float imprecision
123         );
124         my $decimal = ($unit % 3) == ($unit < 0);
125         $unit -= log($decimal ? .995 : .9995) / log(10);  # rounded
126         $decimal = ($unit % 3) == ($unit < 0);
127         $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/;  # integer 0..999
128         sprintf('%*.*f%1s',
129                 3 + ($_[0] < 0), # digits plus optional negative sign
130                 $decimal,  # tenths
131                 $_[0] / 1000 ** int($unit/3),  # number
132                 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
133                         $opt{units}->[$unit/3]  # suffix
134         );
135 };
136
137
138 my (@lines, @values, @order);
139
140 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
141 $SIG{ALRM} = sub {
142         show_lines();
143         alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
144 };
145 $SIG{INT} = \&show_exit;
146
147 if (defined $opt{interval}) {
148         $opt{interval} ||= 1;
149         alarm $opt{interval} if $opt{interval} > 0;
150
151         eval {
152                 require Tie::Array::Sorted;
153                 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
154         } or warn $@, "Expect slowdown with large datasets!\n";
155 }
156
157 my $valmatch = qr<
158         $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
159 >x;
160 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
161         s/\r?\n\z//;
162         s/\A\h*// unless $opt{unmodified};
163         my $valnum = s/$valmatch/\n/ && $1;
164         push @values, $valnum;
165         push @order, $valnum if length $valnum;
166         if (defined $opt{trim} and defined $valnum) {
167                 my $trimpos = abs $opt{trim};
168                 $trimpos -= length $valnum if $opt{unmodified};
169                 if ($trimpos <= 1) {
170                         $_ = substr $_, 0, 2;
171                 }
172                 elsif (length > $trimpos) {
173                         # cut and replace (intentional lvalue for speed, contrary to PBP)
174                         substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
175                 }
176         }
177         push @lines, $_;
178         show_lines() if defined $opt{interval} and $opt{interval} < 0
179                 and $. % $opt{interval} == 0;
180 }
181
182 if ($opt{'zero-missing'}) {
183         push @values, (0) x 10;
184 }
185
186 $SIG{INT} = 'DEFAULT';
187
188 sub color {
189         $opt{color} and defined $_[0] or return '';
190         return "\e[$_[0]m" if defined wantarray;
191         $_ = color(@_) . $_ . color(0) if defined;
192 }
193
194 sub show_lines {
195
196 state $nr =
197         $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
198         $opt{hidemin};
199 @lines > $nr or return;
200
201 my $limit = $#lines;
202 if (defined $opt{hidemax}) {
203         if ($opt{hidemin} and $opt{hidemin} < 0) {
204                 $limit -= $opt{hidemax} - 1;
205         }
206         else {
207                 $limit = $opt{hidemax} - 1;
208         }
209 }
210
211 @order = sort { $b <=> $a } @order unless tied @order;
212 my $maxval = $opt{maxval} // (
213         $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
214         $order[0]
215 ) // 0;
216 my $minval = $opt{minval} // min $order[-1] // (), 0;
217 my $range = $maxval - $minval;
218 my $lenval = $opt{'value-length'} // max map { length } @order;
219 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
220         max map { length $values[$_] && length $lines[$_] }
221                 0 .. min $#lines, $opt{hidemax} || ();  # left padding
222 my $size   = defined $opt{width} && $range &&
223         ($opt{width} - $lenval - $len) / $range;  # bar multiplication
224
225 my @barmark;
226 if ($opt{markers} and $size > 0) {
227         for my $markspec (split /\h/, $opt{markers}) {
228                 my ($char, $func) = split //, $markspec, 2;
229                 my $pos = eval {
230                         if ($func eq 'avg') {
231                                 return sum(@order) / @order;
232                         }
233                         elsif ($func =~ /\A([0-9.]+)v\z/) {
234                                 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
235                                 my $index = $#order * $1 / 100;
236                                 return ($order[$index] + $order[$index + .5]) / 2;
237                         }
238                         elsif ($func =~ /\A-?[0-9.]+\z/) {
239                                 return $func;
240                         }
241                         else {
242                                 die "Unknown marker $char: $func\n";
243                         }
244                 };
245                 defined $pos or do {
246                         warn $@ if $@;
247                         next;
248                 };
249                 $pos -= $minval;
250                 $pos >= 0 or next;
251                 color(36) for $barmark[$pos * $size] = $char;
252         }
253
254         state $lastmax = $maxval;
255         if ($maxval > $lastmax) {
256                 print ' ' x ($lenval + $len);
257                 printf color(90);
258                 printf '%-*s',
259                         ($lastmax - $minval) * $size + .5,
260                         '-' x (($values[$nr - 1] - $minval) * $size);
261                 print color(92);
262                 say '+' x (($range - $lastmax) * $size + .5);
263                 print color(0);
264                 $lastmax = $maxval;
265         }
266 }
267
268 say(
269         color(31), sprintf('%*s', $lenval, $minval),
270         color(90), '-', color(36), '+',
271         color(32), sprintf('%*s', $size * $range - 3, $maxval),
272         color(90), '-', color(36), '+',
273         color(0),
274 ) if $opt{header};
275
276 while ($nr <= $limit) {
277         my $val = $values[$nr];
278         my $rel = length $val && $range && ($val - $minval) / $range;
279         my $color = !length $val || !$opt{palette} ? undef :
280                 $val == $order[0] ? $opt{palette}->[-1] : # max
281                 $val == $order[-1] ? $opt{palette}->[0] : # min
282                 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
283
284         if ($opt{spark}) {
285                 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
286                 print color($color), $opt{spark}->[
287                         !$val || !$#{$opt{spark}} ? 0 : # blank
288                         $val == $order[0] ? -1 : # max
289                         $val == $order[-1] ? 1 : # min
290                         $#{$opt{spark}} < 3 ? 1 :
291                         $rel * ($#{$opt{spark}} - 3) + 2.5
292                 ];
293                 next;
294         }
295
296         if (length $val) {
297                 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
298                         sprintf "%*s", $lenval, $val;
299                 color($color) for $val;
300         }
301         my $line = $lines[$nr] =~ s/\n/$val/r;
302         if (not length $val) {
303                 say $line;
304                 next;
305         }
306         printf '%-*s', $len + length($val), $line;
307         print $barmark[$_] // $opt{'graph-format'}
308                 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
309         say '';
310 }
311 continue {
312         $nr++;
313 }
314 say $opt{palette} ? color(0) : '' if $opt{spark};
315
316         return $nr;
317 }
318
319 sub show_stat {
320         if ($opt{hidemin} or $opt{hidemax}) {
321                 my $linemin = $opt{hidemin};
322                 my $linemax = ($opt{hidemax} || @lines) - 1;
323                 if ($linemin < 0) {
324                         $linemin += @lines;
325                         $linemax = @lines - $linemax;
326                 }
327                 printf '%.8g of ', $opt{'sum-format'}->(
328                         sum(grep {length} @values[$linemin .. $linemax]) // 0
329                 );
330         }
331         if (@order) {
332                 my $total = sum @order;
333                 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
334                 printf ' in %d values', scalar @order;
335                 printf ' over %d lines', scalar @lines if @order != @lines;
336                 printf(' (%s min, %s avg, %s max)',
337                         color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
338                         color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
339                         color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
340                 );
341         }
342         say '';
343         return 1;
344 }
345
346 sub show_exit {
347         show_lines();
348         show_stat() if $opt{stat};
349         exit 130 if @_;  # 0x80+signo
350         exit;
351 }
352
353 show_exit();
354
355 __END__
356 Usage:                                               /\_/\
357   barcat [OPTIONS] [FILES|NUMBERS]                  (=•.•=)
358                                                     (u   u)
359 Options:
360   -a, --[no-]ascii         Restrict user interface to ASCII characters
361   -c, --[no-]color         Force colored output of values and bar markers
362   -f, --field=(N|REGEXP)   Compare values after a given number of whitespace
363                            separators
364       --header             Prepend a chart axis with minimum and maximum
365                            values labeled
366   -H, --human-readable     Format values using SI unit prefixes
367   -t, --interval[=(N|-LINES)]
368                            Output partial progress every given number of
369                            seconds or input lines
370   -l, --length=[-]SIZE[%]  Trim line contents (between number and bars)
371   -L, --limit[=(N|-LAST|START-[END])]
372                            Stop output after a number of lines
373       --graph-format=CHAR  Glyph to repeat for the graph line
374   -m, --markers=FORMAT     Statistical positions to indicate on bars
375       --min=N, --max=N     Bars extend from 0 or the minimum value if lower
376       --palette=(PRESET|COLORS)
377                            Override colors of parsed numbers
378       --spark[=CHARS]      Replace lines by sparklines
379   -s, --stat               Total statistics after all data
380   -u, --unmodified         Do not reformat values, keeping leading whitespace
381       --value-length=SIZE  Reserved space for numbers
382   -w, --width=COLUMNS      Override the maximum number of columns to use
383   -h, --usage              Overview of available options
384       --help               Full pod documentation
385   -V, --version            Version information
386
387 =encoding utf8
388
389 =head1 NAME
390
391 barcat - concatenate texts with graph to visualize values
392
393 =head1 SYNOPSIS
394
395 B<barcat> [<options>] [<file>... | <numbers>]
396
397 =head1 DESCRIPTION
398
399 Visualizes relative sizes of values read from input
400 (parameters, file(s) or STDIN).
401 Contents are concatenated similar to I<cat>,
402 but numbers are reformatted and a bar graph is appended to each line.
403
404 Don't worry, barcat does not drink and divide.
405 It can has various options for input and output (re)formatting,
406 but remains limited to one-dimensional charts.
407 For more complex graphing needs
408 you'll need a larger animal like I<gnuplot>.
409
410 =head1 OPTIONS
411
412 =over
413
414 =item -a, --[no-]ascii
415
416 Restrict user interface to ASCII characters,
417 replacing default UTF-8 by their closest approximation.
418 Input is always interpreted as UTF-8 and shown as is.
419
420 =item -c, --[no-]color
421
422 Force colored output of values and bar markers.
423 Defaults on if output is a tty,
424 disabled otherwise such as when piped or redirected.
425
426 =item -f, --field=(<number> | <regexp>)
427
428 Compare values after a given number of whitespace separators,
429 or matching a regular expression.
430
431 Unspecified or I<-f0> means values are at the start of each line.
432 With I<-f1> the second word is taken instead.
433 A string can indicate the starting position of a value
434 (such as I<-f:> if preceded by colons),
435 or capture the numbers itself,
436 for example I<-f'(\d+)'> for the first digits anywhere.
437
438 =item --header
439
440 Prepend a chart axis with minimum and maximum values labeled.
441
442 =item -H, --human-readable
443
444 Format values using SI unit prefixes,
445 turning long numbers like I<12356789> into I<12.4M>.
446 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
447 Short integers are aligned but kept without decimal point.
448
449 =item -t, --interval[=(<seconds> | -<lines>)]
450
451 Output partial progress every given number of seconds or input lines.
452 An update can also be forced by sending a I<SIGALRM> alarm signal.
453
454 =item -l, --length=[-]<size>[%]
455
456 Trim line contents (between number and bars)
457 to a maximum number of characters.
458 The exceeding part is replaced by an abbreviation sign,
459 unless C<--length=0>.
460
461 Prepend a dash (i.e. make negative) to enforce padding
462 regardless of encountered contents.
463
464 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
465
466 Stop output after a number of lines.
467 A single value indicates the last line number (like C<head>),
468 or first line counting from the bottom if negative (like C<tail>).
469 A specific range can be given by two values.
470
471 All input is still counted and analyzed for statistics,
472 but disregarded for padding and bar size.
473
474 =item --graph-format=<character>
475
476 Glyph to repeat for the graph line.
477 Defaults to a dash C<->.
478
479 =item -m, --markers=<format>
480
481 Statistical positions to indicate on bars.
482 A single indicator glyph precedes each position:
483
484 =over 2
485
486 =item <number>
487
488 Exact value to match on the axis.
489 A vertical bar at the zero crossing is displayed by I<|0>
490 for negative values.
491 For example I<:3.14> would show a colon at pi.
492
493 =item <percentage>I<v>
494
495 Ranked value at the given percentile.
496 The default shows I<+> at I<50v> for the mean or median;
497 the middle value or average between middle values.
498 One standard deviation right of the mean is at about I<68.3v>.
499 The default includes I<< >31.73v <68.27v >>
500 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
501
502 =item I<avg>
503
504 Matches the average;
505 the sum of all values divided by the number of counted lines.
506 Indicated by default as I<=>.
507
508 =back
509
510 =item --min=<number>, --max=<number>
511
512 Bars extend from 0 or the minimum value if lower,
513 to the largest value encountered.
514 These options can be set to customize this range.
515
516 =item --palette=(<preset> | <color>...)
517
518 Override colors of parsed numbers.
519 Can be any CSI escape, such as I<90> for default dark grey,
520 or alternatively I<1;30> for bright black.
521
522 In case of additional colors,
523 the last is used for values equal to the maximum, the first for minima.
524 If unspecified, these are green and red respectively (I<31 90 32>).
525 Multiple intermediate colors will be distributed
526 relative to the size of values.
527
528 Predefined color schemes are named I<whites> and I<fire>,
529 or I<greys> and I<fire256> for 256-color variants.
530
531 =item --spark[=<characters>]
532
533 Replace lines by I<sparklines>,
534 single characters corresponding to input values.
535 A specified sequence of unicode characters will be used for
536 Of a specified sequence of unicode characters,
537 the first one will be used for non-values,
538 the last one for the maximum,
539 the second (if any) for the minimum,
540 and any remaining will be distributed over the range of values.
541 Unspecified, block fill glyphs U+2581-2588 will be used.
542
543 =item -s, --stat
544
545 Total statistics after all data.
546
547 =item -u, --unmodified
548
549 Do not reformat values, keeping leading whitespace.
550 Keep original value alignment, which may be significant in some programs.
551
552 =item --value-length=<size>
553
554 Reserved space for numbers.
555
556 =item -w, --width=<columns>
557
558 Override the maximum number of columns to use.
559 Appended graphics will extend to fill up the entire screen.
560
561 =item -h, --usage
562
563 Overview of available options.
564
565 =item --help
566
567 Full pod documentation
568 as rendered by perldoc.
569
570 =item -V, --version
571
572 Version information.
573
574 =back
575
576 =head1 EXAMPLES
577
578 Draw a sine wave:
579
580     seq 30 | awk '{print sin($1/10)}' | barcat
581
582 Compare file sizes (with human-readable numbers):
583
584     du -d0 -b * | barcat -H
585
586 Memory usage of user processes with long names truncated:
587
588     ps xo %mem,pid,cmd | barcat -l40
589
590 Monitor network latency from prefixed results:
591
592     ping google.com | barcat -f'time=\K' -t
593
594 Commonly used after counting, for example users on the current server:
595
596     users | tr ' ' '\n' | sort | uniq -c | barcat
597
598 Letter frequencies in text files:
599
600     cat /usr/share/games/fortunes/*.u8 |
601     perl -CS -nE 'say for grep length, split /\PL*/, uc' |
602     sort | uniq -c | barcat
603
604 Number of HTTP requests per day:
605
606     cat log/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
607
608 Any kind of database query with counts, preserving returned alignment:
609
610     echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
611     psql -t | barcat -u
612
613 In PostgreSQL from within the client:
614
615         postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
616
617 Earthquakes worldwide magnitude 1+ in the last 24 hours:
618
619     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
620     column -tns, | barcat -f4 -u -l80%
621
622 External datasets, like movies per year:
623
624     curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
625     perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
626
627 But please get I<jq> to process JSON
628 and replace the manual selection by C<< jq '.[].year' >>.
629
630 Pokémon height comparison:
631
632     curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
633     jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
634
635 USD/EUR exchange rate from CSV provided by the ECB:
636
637     curl https://sdw.ecb.europa.eu/export.do \
638          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
639     grep '^[12]' | barcat -f',\K' --value-length=7
640
641 Total population history in XML from the World Bank:
642
643     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
644     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
645     sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
646
647 And of course various Git statistics, such commit count by year:
648
649     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
650
651 Or the top 3 most frequent authors with statistics over all:
652
653     git shortlog -sn | barcat -L3 -s
654
655 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
656
657     ( git log --pretty=%ci --since=30day | cut -b-10
658       seq 0 30 | xargs -i date +%F -d-{}day ) |
659     sort | uniq -c | awk '$1--' | barcat --spark
660
661 Sparkline graphics of simple input given as inline parameters:
662
663         barcat --spark= 3 1 4 1 5 0 9 2 4
664
665 =head1 AUTHOR
666
667 Mischa POSLAWSKY <perl@shiar.org>
668
669 =head1 LICENSE
670
671 GPL3+.