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;
61 fire => [qw( 90 31 91 33 93 97 96 )],
62 fire256=> [map {"38;5;$_"} qw(
64 202 208 214 220 226 227 228 229 230 231 159
66 whites => [qw( 1;30 0;37 1;37 )],
67 greys => [map {"38;5;$_"} 0, 232..255, 15],
68 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
69 rainbow=> [map {"38;5;$_"}
71 (map { 196 + $_*6 } 0..4), # +g
72 (map { 226 - $_*6*6 } 0..4), # -r
73 (map { 46 + $_ } 0..4), # +b
74 (map { 51 - $_*6 } 0..4), # -g
75 (map { 21 + $_*6*6 } 0..4), # +r
76 (map { 201 - $_ } 0..4), # -b
80 my @vals = split /[^0-9;]/, $_[1]
81 or die "Empty palette resulting from \"$_[1]\"\n";
90 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
91 say "barcat $mascot version $VERSION";
95 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
100 Pod::Usage::pod2usage(
101 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
104 ) or exit 64; # EX_USAGE
107 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
108 $opt{color} //= -t *STDOUT; # enable on tty
109 $opt{'graph-format'} //= '-';
110 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
111 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
112 if $opt{'human-readable'};
113 $opt{anchor} //= qr/\A/;
114 $opt{'value-length'} = 6 if $opt{units};
115 $opt{'value-length'} = 1 if $opt{unmodified};
116 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
117 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
118 $opt{palette} //= $opt{color} && [31, 90, 32];
119 $opt{indicators} = [split //, $opt{indicators} ||
120 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
121 ] if defined $opt{indicators} or $opt{spark};
122 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
123 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
124 and undef $opt{interval};
126 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
127 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
128 $opt{'value-format'} = $opt{units} && sub {
130 log(abs $_[0] || 1) / log(10)
131 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
132 + 1e-15 # float imprecision
134 my $decimal = ($unit % 3) == ($unit < 0);
135 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
136 $decimal = ($unit % 3) == ($unit < 0);
137 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
139 3 + ($_[0] < 0), # digits plus optional negative sign
141 $_[0] / 1000 ** int($unit/3), # number
142 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
143 $opt{units}->[$unit/3] # suffix
148 my (@lines, @values, @order);
150 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
153 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
155 $SIG{INT} = \&show_exit;
157 if (defined $opt{interval}) {
158 $opt{interval} ||= 1;
159 alarm $opt{interval} if $opt{interval} > 0;
162 require Tie::Array::Sorted;
163 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
164 } or warn $@, "Expect slowdown with large datasets!\n";
168 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
170 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
172 s/\A\h*// unless $opt{unmodified};
173 my $valnum = s/$valmatch/\n/ && $1;
174 push @values, $valnum;
175 push @order, $valnum if length $valnum;
176 if (defined $opt{trim} and defined $valnum) {
177 my $trimpos = abs $opt{trim};
178 $trimpos -= length $valnum if $opt{unmodified};
180 $_ = substr $_, 0, 2;
182 elsif (length > $trimpos) {
183 # cut and replace (intentional lvalue for speed, contrary to PBP)
184 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
188 show_lines() if defined $opt{interval} and $opt{interval} < 0
189 and $. % $opt{interval} == 0;
192 if ($opt{'zero-missing'}) {
193 push @values, (0) x 10;
196 $SIG{INT} = 'DEFAULT';
199 $opt{color} and defined $_[0] or return '';
200 return "\e[$_[0]m" if defined wantarray;
201 $_ = color(@_) . $_ . color(0) if defined;
207 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
209 @lines > $nr or return;
212 if (defined $opt{hidemax}) {
213 if ($opt{hidemin} and $opt{hidemin} < 0) {
214 $limit -= $opt{hidemax} - 1;
216 elsif ($opt{hidemax} <= $limit) {
217 $limit = $opt{hidemax} - 1;
221 @order = sort { $b <=> $a } @order unless tied @order;
222 my $maxval = $opt{maxval} // (
223 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
226 my $minval = $opt{minval} // min $order[-1] // (), 0;
227 my $range = $maxval - $minval;
228 my $lenval = $opt{'value-length'} // max map { length } @order;
229 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
230 max map { length $values[$_] && length $lines[$_] }
231 0 .. min $#lines, $opt{hidemax} || (); # left padding
232 my $size = defined $opt{width} && $range &&
233 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
236 if ($opt{markers} and $size > 0) {
237 for my $markspec (split /\h/, $opt{markers}) {
238 my ($char, $func) = split //, $markspec, 2;
240 if ($func eq 'avg') {
241 return sum(@order) / @order;
243 elsif ($func =~ /\A([0-9.]+)v\z/) {
244 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
245 my $index = $#order * $1 / 100;
246 return ($order[$index] + $order[$index + .5]) / 2;
248 elsif ($func =~ /\A-?[0-9.]+\z/) {
252 die "Unknown marker $char: $func\n";
261 color(36) for $barmark[$pos * $size] = $char;
264 state $lastmax = $maxval;
265 if ($maxval > $lastmax) {
266 print ' ' x ($lenval + $len);
269 ($lastmax - $minval) * $size + .5,
270 '-' x (($values[$nr - 1] - $minval) * $size);
272 say '+' x (($range - $lastmax) * $size + .5);
279 color(31), sprintf('%*s', $lenval, $minval),
280 color(90), '-', color(36), '+',
281 color(32), sprintf('%*s', $size * $range - 3, $maxval),
282 color(90), '-', color(36), '+',
286 while ($nr <= $limit) {
287 my $val = $values[$nr];
288 my $rel = length $val && $range && ($val - $minval) / $range;
289 my $color = !length $val || !$opt{palette} ? undef :
290 $val == $order[0] ? $opt{palette}->[-1] : # max
291 $val == $order[-1] ? $opt{palette}->[0] : # min
292 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
293 my $indicator = $opt{indicators} && $opt{indicators}->[
294 !$val || !$#{$opt{indicators}} ? 0 : # blank
295 $#{$opt{indicators}} < 2 ? 1 :
296 $val >= $order[0] ? -1 :
297 $rel * ($#{$opt{indicators}} - 1e-14) + 1
301 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
302 print color($color), $_ for $indicator;
305 print $indicator if defined $indicator;
308 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
309 sprintf "%*s", $lenval, $val;
310 color($color) for $val;
312 my $line = $lines[$nr] =~ s/\n/$val/r;
313 if (not length $val) {
317 printf '%-*s', $len + length($val), $line;
318 print $barmark[$_] // $opt{'graph-format'}
319 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
325 say $opt{palette} ? color(0) : '' if $opt{spark};
331 if ($opt{hidemin} or $opt{hidemax}) {
332 my $linemin = $opt{hidemin};
333 my $linemax = ($opt{hidemax} || @lines) - 1;
336 $linemax = @lines - $linemax;
338 printf '%.8g of ', $opt{'sum-format'}->(
339 sum(grep {length} @values[$linemin .. $linemax]) // 0
343 my $total = sum @order;
344 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
345 printf ' in %d values', scalar @order;
346 printf ' over %d lines', scalar @lines if @order != @lines;
347 printf(' (%s min, %s avg, %s max)',
348 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
349 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
350 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
359 show_stat() if $opt{stat};
360 exit 130 if @_; # 0x80+signo
368 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
371 -a, --[no-]ascii Restrict user interface to ASCII characters
372 -c, --[no-]color Force colored output of values and bar markers
373 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
375 --header Prepend a chart axis with minimum and maximum
377 -H, --human-readable Format values using SI unit prefixes
378 -t, --interval[=(N|-LINES)]
379 Output partial progress every given number of
380 seconds or input lines
381 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
382 -L, --limit[=(N|-LAST|START-[END])]
383 Stop output after a number of lines
384 --graph-format=CHAR Glyph to repeat for the graph line
385 -m, --markers=FORMAT Statistical positions to indicate on bars
386 --min=N, --max=N Bars extend from 0 or the minimum value if lower
387 --palette=(PRESET|COLORS)
388 Override colors of parsed numbers
389 -_, --spark Replace lines by sparklines
390 --indicators[=CHARS] Prefix a unicode character corresponding to each
392 -s, --stat Total statistics after all data
393 -u, --unmodified Do not reformat values, keeping leading whitespace
394 --value-length=SIZE Reserved space for numbers
395 -w, --width=COLUMNS Override the maximum number of columns to use
396 -h, --usage Overview of available options
397 --help Full pod documentation
398 -V, --version Version information
404 barcat - concatenate texts with graph to visualize values
408 B<barcat> [<options>] [<file>... | <numbers>]
412 Visualizes relative sizes of values read from input
413 (parameters, file(s) or STDIN).
414 Contents are concatenated similar to I<cat>,
415 but numbers are reformatted and a bar graph is appended to each line.
417 Don't worry, barcat does not drink and divide.
418 It can has various options for input and output (re)formatting,
419 but remains limited to one-dimensional charts.
420 For more complex graphing needs
421 you'll need a larger animal like I<gnuplot>.
427 =item -a, --[no-]ascii
429 Restrict user interface to ASCII characters,
430 replacing default UTF-8 by their closest approximation.
431 Input is always interpreted as UTF-8 and shown as is.
433 =item -c, --[no-]color
435 Force colored output of values and bar markers.
436 Defaults on if output is a tty,
437 disabled otherwise such as when piped or redirected.
439 =item -f, --field=(<number> | <regexp>)
441 Compare values after a given number of whitespace separators,
442 or matching a regular expression.
444 Unspecified or I<-f0> means values are at the start of each line.
445 With I<-f1> the second word is taken instead.
446 A string can indicate the starting position of a value
447 (such as I<-f:> if preceded by colons),
448 or capture the numbers itself,
449 for example I<-f'(\d+)'> for the first digits anywhere.
453 Prepend a chart axis with minimum and maximum values labeled.
455 =item -H, --human-readable
457 Format values using SI unit prefixes,
458 turning long numbers like I<12356789> into I<12.4M>.
459 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
460 Short integers are aligned but kept without decimal point.
462 =item -t, --interval[=(<seconds> | -<lines>)]
464 Output partial progress every given number of seconds or input lines.
465 An update can also be forced by sending a I<SIGALRM> alarm signal.
467 =item -l, --length=[-]<size>[%]
469 Trim line contents (between number and bars)
470 to a maximum number of characters.
471 The exceeding part is replaced by an abbreviation sign,
472 unless C<--length=0>.
474 Prepend a dash (i.e. make negative) to enforce padding
475 regardless of encountered contents.
477 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
479 Stop output after a number of lines.
480 A single value indicates the last line number (like C<head>),
481 or first line counting from the bottom if negative (like C<tail>).
482 A specific range can be given by two values.
484 All input is still counted and analyzed for statistics,
485 but disregarded for padding and bar size.
487 =item --graph-format=<character>
489 Glyph to repeat for the graph line.
490 Defaults to a dash C<->.
492 =item -m, --markers=<format>
494 Statistical positions to indicate on bars.
495 A single indicator glyph precedes each position:
501 Exact value to match on the axis.
502 A vertical bar at the zero crossing is displayed by I<|0>
504 For example I<:3.14> would show a colon at pi.
506 =item <percentage>I<v>
508 Ranked value at the given percentile.
509 The default shows I<+> at I<50v> for the mean or median;
510 the middle value or average between middle values.
511 One standard deviation right of the mean is at about I<68.3v>.
512 The default includes I<< >31.73v <68.27v >>
513 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
518 the sum of all values divided by the number of counted lines.
519 Indicated by default as I<=>.
523 =item --min=<number>, --max=<number>
525 Bars extend from 0 or the minimum value if lower,
526 to the largest value encountered.
527 These options can be set to customize this range.
529 =item --palette=(<preset> | <color>...)
531 Override colors of parsed numbers.
532 Can be any CSI escape, such as I<90> for default dark grey,
533 or alternatively I<1;30> for bright black.
535 In case of additional colors,
536 the last is used for values equal to the maximum, the first for minima.
537 If unspecified, these are green and red respectively (I<31 90 32>).
538 Multiple intermediate colors will be distributed
539 relative to the size of values.
541 Predefined color schemes are named I<whites> and I<fire>,
542 or I<greys> and I<fire256> for 256-color variants.
546 Replace lines by I<sparklines>,
547 single characters (configured by C<--indicators>)
548 corresponding to input values.
550 =item --indicators[=<characters>]
552 Prefix a unicode character corresponding to each value.
553 The first specified character will be used for non-values,
554 the remaining sequence will be distributed over the range of values.
555 Unspecified, block fill glyphs U+2581-2588 will be used.
559 Total statistics after all data.
561 =item -u, --unmodified
563 Do not reformat values, keeping leading whitespace.
564 Keep original value alignment, which may be significant in some programs.
566 =item --value-length=<size>
568 Reserved space for numbers.
570 =item -w, --width=<columns>
572 Override the maximum number of columns to use.
573 Appended graphics will extend to fill up the entire screen.
577 Overview of available options.
581 Full pod documentation
582 as rendered by perldoc.
594 seq 30 | awk '{print sin($1/10)}' | barcat
596 Compare file sizes (with human-readable numbers):
598 du -d0 -b * | barcat -H
600 Memory usage of user processes with long names truncated:
602 ps xo rss,pid,cmd | barcat -l40
604 Monitor network latency from prefixed results:
606 ping google.com | barcat -f'time=\K' -t
608 Commonly used after counting, for example users on the current server:
610 users | tr ' ' '\n' | sort | uniq -c | barcat
612 Letter frequencies in text files:
614 cat /usr/share/games/fortunes/*.u8 |
615 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
616 sort | uniq -c | barcat
618 Number of HTTP requests per day:
620 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
622 Any kind of database query with counts, preserving returned alignment:
624 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
627 In PostgreSQL from within the client:
629 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
631 Earthquakes worldwide magnitude 1+ in the last 24 hours:
633 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
634 column -tns, | barcat -f4 -u -l80%
636 External datasets, like movies per year:
638 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
639 jq .[].year | uniq -c | barcat
641 Pokémon height comparison:
643 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
644 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
646 USD/EUR exchange rate from CSV provided by the ECB:
648 curl https://sdw.ecb.europa.eu/export.do \
649 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
650 barcat -f',\K' --value-length=7
652 Total population history in XML from the World Bank:
654 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
655 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
658 And of course various Git statistics, such commit count by year:
660 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
662 Or the top 3 most frequent authors with statistics over all:
664 git shortlog -sn | barcat -L3 -s
666 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
668 ( git log --pretty=%ci --since=30day | cut -b-10
669 seq 0 30 | xargs -i date +%F -d-{}day ) |
670 sort | uniq -c | awk '$1--' | barcat --spark
672 Sparkline graphics of simple input given as inline parameters:
674 barcat -_ 3 1 4 1 5 0 9 2 4
676 Misusing the spark functionality to draw a lolcat line:
678 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
682 Mischa POSLAWSKY <perl@shiar.org>