5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
14 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
18 'C' => sub { $opt{color} = 0 },
22 $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
23 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
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"
42 my ($optname, $optval) = @_;
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",
53 'graph-format=s' => sub {
54 $opt{'graph-format'} = substr $_[1], 0, 1;
60 fire => [qw( 90 31 91 33 93 97 96 )],
61 fire256=> [map {"38;5;$_"} qw(
63 202 208 214 220 226 227 228 229 230 231 159
65 whites => [qw( 1;30 0;37 1;37 )],
66 greys => [map {"38;5;$_"} 0, 232..255, 15],
67 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
68 rainbow=> [map {"38;5;$_"}
70 (map { 196 + $_*6 } 0..4), # +g
71 (map { 226 - $_*6*6 } 0..4), # -r
72 (map { 46 + $_ } 0..4), # +b
73 (map { 51 - $_*6 } 0..4), # -g
74 (map { 21 + $_*6*6 } 0..4), # +r
75 (map { 201 - $_ } 0..4), # -b
78 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
85 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
86 say "barcat $mascot version $VERSION";
90 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
95 Pod::Usage::pod2usage(
96 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
99 ) or exit 64; # EX_USAGE
102 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
103 $opt{color} //= -t *STDOUT; # enable on tty
104 $opt{'graph-format'} //= '-';
105 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
106 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
107 if $opt{'human-readable'};
108 $opt{anchor} //= qr/\A/;
109 $opt{'value-length'} = 6 if $opt{units};
110 $opt{'value-length'} = 1 if $opt{unmodified};
111 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
112 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
113 $opt{palette} //= $opt{color} && [31, 90, 32];
114 $opt{indicators} = [split //, $opt{indicators} ||
115 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
116 ] if defined $opt{indicators} or $opt{spark};
117 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
118 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
119 and undef $opt{interval};
121 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
122 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
123 $opt{'value-format'} = $opt{units} && sub {
125 log(abs $_[0] || 1) / log(10)
126 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
127 + 1e-15 # float imprecision
129 my $decimal = ($unit % 3) == ($unit < 0);
130 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
131 $decimal = ($unit % 3) == ($unit < 0);
132 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
134 3 + ($_[0] < 0), # digits plus optional negative sign
136 $_[0] / 1000 ** int($unit/3), # number
137 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
138 $opt{units}->[$unit/3] # suffix
143 my (@lines, @values, @order);
145 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
148 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
150 $SIG{INT} = \&show_exit;
152 if (defined $opt{interval}) {
153 $opt{interval} ||= 1;
154 alarm $opt{interval} if $opt{interval} > 0;
157 require Tie::Array::Sorted;
158 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
159 } or warn $@, "Expect slowdown with large datasets!\n";
163 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
165 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
167 s/\A\h*// unless $opt{unmodified};
168 my $valnum = s/$valmatch/\n/ && $1;
169 push @values, $valnum;
170 push @order, $valnum if length $valnum;
171 if (defined $opt{trim} and defined $valnum) {
172 my $trimpos = abs $opt{trim};
173 $trimpos -= length $valnum if $opt{unmodified};
175 $_ = substr $_, 0, 2;
177 elsif (length > $trimpos) {
178 # cut and replace (intentional lvalue for speed, contrary to PBP)
179 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
183 show_lines() if defined $opt{interval} and $opt{interval} < 0
184 and $. % $opt{interval} == 0;
187 if ($opt{'zero-missing'}) {
188 push @values, (0) x 10;
191 $SIG{INT} = 'DEFAULT';
194 $opt{color} and defined $_[0] or return '';
195 return "\e[$_[0]m" if defined wantarray;
196 $_ = color(@_) . $_ . color(0) if defined;
202 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
204 @lines > $nr or return;
207 if (defined $opt{hidemax}) {
208 if ($opt{hidemin} and $opt{hidemin} < 0) {
209 $limit -= $opt{hidemax} - 1;
211 elsif ($opt{hidemax} <= $limit) {
212 $limit = $opt{hidemax} - 1;
216 @order = sort { $b <=> $a } @order unless tied @order;
217 my $maxval = $opt{maxval} // (
218 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
221 my $minval = $opt{minval} // min $order[-1] // (), 0;
222 my $range = $maxval - $minval;
223 my $lenval = $opt{'value-length'} // max map { length } @order;
224 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
225 max map { length $values[$_] && length $lines[$_] }
226 0 .. min $#lines, $opt{hidemax} || (); # left padding
227 my $size = defined $opt{width} && $range &&
228 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
231 if ($opt{markers} and $size > 0) {
232 for my $markspec (split /\h/, $opt{markers}) {
233 my ($char, $func) = split //, $markspec, 2;
235 if ($func eq 'avg') {
236 return sum(@order) / @order;
238 elsif ($func =~ /\A([0-9.]+)v\z/) {
239 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
240 my $index = $#order * $1 / 100;
241 return ($order[$index] + $order[$index + .5]) / 2;
243 elsif ($func =~ /\A-?[0-9.]+\z/) {
247 die "Unknown marker $char: $func\n";
256 color(36) for $barmark[$pos * $size] = $char;
259 state $lastmax = $maxval;
260 if ($maxval > $lastmax) {
261 print ' ' x ($lenval + $len);
264 ($lastmax - $minval) * $size + .5,
265 '-' x (($values[$nr - 1] - $minval) * $size);
267 say '+' x (($range - $lastmax) * $size + .5);
274 color(31), sprintf('%*s', $lenval, $minval),
275 color(90), '-', color(36), '+',
276 color(32), sprintf('%*s', $size * $range - 3, $maxval),
277 color(90), '-', color(36), '+',
281 while ($nr <= $limit) {
282 my $val = $values[$nr];
283 my $rel = length $val && $range && ($val - $minval) / $range;
284 my $color = !length $val || !$opt{palette} ? undef :
285 $val == $order[0] ? $opt{palette}->[-1] : # max
286 $val == $order[-1] ? $opt{palette}->[0] : # min
287 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
288 my $indicator = $opt{indicators} && $opt{indicators}->[
289 !$val || !$#{$opt{indicators}} ? 0 : # blank
290 $#{$opt{indicators}} < 2 ? 1 :
291 $val >= $order[0] ? -1 :
292 $rel * ($#{$opt{indicators}} - 1e-14) + 1
296 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
297 print color($color), $_ for $indicator;
300 print $indicator if defined $indicator;
303 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
304 sprintf "%*s", $lenval, $val;
305 color($color) for $val;
307 my $line = $lines[$nr] =~ s/\n/$val/r;
308 if (not length $val) {
312 printf '%-*s', $len + length($val), $line;
313 print $barmark[$_] // $opt{'graph-format'}
314 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
320 say $opt{palette} ? color(0) : '' if $opt{spark};
326 if ($opt{hidemin} or $opt{hidemax}) {
327 my $linemin = $opt{hidemin};
328 my $linemax = ($opt{hidemax} || @lines) - 1;
331 $linemax = @lines - $linemax;
333 printf '%.8g of ', $opt{'sum-format'}->(
334 sum(grep {length} @values[$linemin .. $linemax]) // 0
338 my $total = sum @order;
339 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
340 printf ' in %d values', scalar @order;
341 printf ' over %d lines', scalar @lines if @order != @lines;
342 printf(' (%s min, %s avg, %s max)',
343 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
344 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
345 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
354 show_stat() if $opt{stat};
355 exit 130 if @_; # 0x80+signo
363 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
366 -a, --[no-]ascii Restrict user interface to ASCII characters
367 -c, --[no-]color Force colored output of values and bar markers
368 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
370 --header Prepend a chart axis with minimum and maximum
372 -H, --human-readable Format values using SI unit prefixes
373 -t, --interval[=(N|-LINES)]
374 Output partial progress every given number of
375 seconds or input lines
376 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
377 -L, --limit[=(N|-LAST|START-[END])]
378 Stop output after a number of lines
379 --graph-format=CHAR Glyph to repeat for the graph line
380 -m, --markers=FORMAT Statistical positions to indicate on bars
381 --min=N, --max=N Bars extend from 0 or the minimum value if lower
382 --palette=(PRESET|COLORS)
383 Override colors of parsed numbers
384 -_, --spark Replace lines by sparklines
385 --indicators[=CHARS] Prefix a unicode character corresponding to each
387 -s, --stat Total statistics after all data
388 -u, --unmodified Do not reformat values, keeping leading whitespace
389 --value-length=SIZE Reserved space for numbers
390 -w, --width=COLUMNS Override the maximum number of columns to use
391 -h, --usage Overview of available options
392 --help Full pod documentation
393 -V, --version Version information
399 barcat - concatenate texts with graph to visualize values
403 B<barcat> [<options>] [<file>... | <numbers>]
407 Visualizes relative sizes of values read from input
408 (parameters, file(s) or STDIN).
409 Contents are concatenated similar to I<cat>,
410 but numbers are reformatted and a bar graph is appended to each line.
412 Don't worry, barcat does not drink and divide.
413 It can has various options for input and output (re)formatting,
414 but remains limited to one-dimensional charts.
415 For more complex graphing needs
416 you'll need a larger animal like I<gnuplot>.
422 =item -a, --[no-]ascii
424 Restrict user interface to ASCII characters,
425 replacing default UTF-8 by their closest approximation.
426 Input is always interpreted as UTF-8 and shown as is.
428 =item -c, --[no-]color
430 Force colored output of values and bar markers.
431 Defaults on if output is a tty,
432 disabled otherwise such as when piped or redirected.
434 =item -f, --field=(<number> | <regexp>)
436 Compare values after a given number of whitespace separators,
437 or matching a regular expression.
439 Unspecified or I<-f0> means values are at the start of each line.
440 With I<-f1> the second word is taken instead.
441 A string can indicate the starting position of a value
442 (such as I<-f:> if preceded by colons),
443 or capture the numbers itself,
444 for example I<-f'(\d+)'> for the first digits anywhere.
448 Prepend a chart axis with minimum and maximum values labeled.
450 =item -H, --human-readable
452 Format values using SI unit prefixes,
453 turning long numbers like I<12356789> into I<12.4M>.
454 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
455 Short integers are aligned but kept without decimal point.
457 =item -t, --interval[=(<seconds> | -<lines>)]
459 Output partial progress every given number of seconds or input lines.
460 An update can also be forced by sending a I<SIGALRM> alarm signal.
462 =item -l, --length=[-]<size>[%]
464 Trim line contents (between number and bars)
465 to a maximum number of characters.
466 The exceeding part is replaced by an abbreviation sign,
467 unless C<--length=0>.
469 Prepend a dash (i.e. make negative) to enforce padding
470 regardless of encountered contents.
472 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
474 Stop output after a number of lines.
475 A single value indicates the last line number (like C<head>),
476 or first line counting from the bottom if negative (like C<tail>).
477 A specific range can be given by two values.
479 All input is still counted and analyzed for statistics,
480 but disregarded for padding and bar size.
482 =item --graph-format=<character>
484 Glyph to repeat for the graph line.
485 Defaults to a dash C<->.
487 =item -m, --markers=<format>
489 Statistical positions to indicate on bars.
490 A single indicator glyph precedes each position:
496 Exact value to match on the axis.
497 A vertical bar at the zero crossing is displayed by I<|0>
499 For example I<:3.14> would show a colon at pi.
501 =item <percentage>I<v>
503 Ranked value at the given percentile.
504 The default shows I<+> at I<50v> for the mean or median;
505 the middle value or average between middle values.
506 One standard deviation right of the mean is at about I<68.3v>.
507 The default includes I<< >31.73v <68.27v >>
508 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
513 the sum of all values divided by the number of counted lines.
514 Indicated by default as I<=>.
518 =item --min=<number>, --max=<number>
520 Bars extend from 0 or the minimum value if lower,
521 to the largest value encountered.
522 These options can be set to customize this range.
524 =item --palette=(<preset> | <color>...)
526 Override colors of parsed numbers.
527 Can be any CSI escape, such as I<90> for default dark grey,
528 or alternatively I<1;30> for bright black.
530 In case of additional colors,
531 the last is used for values equal to the maximum, the first for minima.
532 If unspecified, these are green and red respectively (I<31 90 32>).
533 Multiple intermediate colors will be distributed
534 relative to the size of values.
536 Predefined color schemes are named I<whites> and I<fire>,
537 or I<greys> and I<fire256> for 256-color variants.
541 Replace lines by I<sparklines>,
542 single characters (configured by C<--indicators>)
543 corresponding to input values.
545 =item --indicators[=<characters>]
547 Prefix a unicode character corresponding to each value.
548 The first specified character will be used for non-values,
549 the remaining sequence will be distributed over the range of values.
550 Unspecified, block fill glyphs U+2581-2588 will be used.
554 Total statistics after all data.
556 =item -u, --unmodified
558 Do not reformat values, keeping leading whitespace.
559 Keep original value alignment, which may be significant in some programs.
561 =item --value-length=<size>
563 Reserved space for numbers.
565 =item -w, --width=<columns>
567 Override the maximum number of columns to use.
568 Appended graphics will extend to fill up the entire screen.
572 Overview of available options.
576 Full pod documentation
577 as rendered by perldoc.
589 seq 30 | awk '{print sin($1/10)}' | barcat
591 Compare file sizes (with human-readable numbers):
593 du -d0 -b * | barcat -H
595 Memory usage of user processes with long names truncated:
597 ps xo rss,pid,cmd | barcat -l40
599 Monitor network latency from prefixed results:
601 ping google.com | barcat -f'time=\K' -t
603 Commonly used after counting, for example users on the current server:
605 users | tr ' ' '\n' | sort | uniq -c | barcat
607 Letter frequencies in text files:
609 cat /usr/share/games/fortunes/*.u8 |
610 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
611 sort | uniq -c | barcat
613 Number of HTTP requests per day:
615 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
617 Any kind of database query with counts, preserving returned alignment:
619 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
622 In PostgreSQL from within the client:
624 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
626 Earthquakes worldwide magnitude 1+ in the last 24 hours:
628 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
629 column -tns, | barcat -f4 -u -l80%
631 External datasets, like movies per year:
633 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
634 jq .[].year | uniq -c | barcat
636 Pokémon height comparison:
638 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
639 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
641 USD/EUR exchange rate from CSV provided by the ECB:
643 curl https://sdw.ecb.europa.eu/export.do \
644 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
645 barcat -f',\K' --value-length=7
647 Total population history in XML from the World Bank:
649 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
650 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
653 And of course various Git statistics, such commit count by year:
655 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
657 Or the top 3 most frequent authors with statistics over all:
659 git shortlog -sn | barcat -L3 -s
661 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
663 ( git log --pretty=%ci --since=30day | cut -b-10
664 seq 0 30 | xargs -i date +%F -d-{}day ) |
665 sort | uniq -c | awk '$1--' | barcat --spark
667 Sparkline graphics of simple input given as inline parameters:
669 barcat -_ 3 1 4 1 5 0 9 2 4
671 Misusing the spark functionality to draw a lolcat line:
673 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
677 Mischa POSLAWSKY <perl@shiar.org>