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