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 rainbow=> [map {"38;5;$_"}
69 (map { 196 + $_*6 } 0..4), # +g
70 (map { 226 - $_*6*6 } 0..4), # -r
71 (map { 46 + $_ } 0..4), # +b
72 (map { 51 - $_*6 } 0..4), # -g
73 (map { 21 + $_*6*6 } 0..4), # +r
74 (map { 201 - $_ } 0..4), # -b
77 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
84 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
85 say "barcat $mascot version $VERSION";
89 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
94 Pod::Usage::pod2usage(
95 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
98 ) or exit 64; # EX_USAGE
101 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
102 $opt{color} //= -t *STDOUT; # enable on tty
103 $opt{'graph-format'} //= '-';
104 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
105 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
106 if $opt{'human-readable'};
107 $opt{anchor} //= qr/\A/;
108 $opt{'value-length'} = 6 if $opt{units};
109 $opt{'value-length'} = 1 if $opt{unmodified};
110 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
111 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
112 $opt{palette} //= $opt{color} && [31, 90, 32];
113 $opt{indicators} = [split //,
114 $opt{indicators} || ($opt{ascii} ? ' .oO' : ' ▁▂▃▄▅▆▇█')
115 ] if defined $opt{indicators} or $opt{spark};
116 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
117 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
118 and undef $opt{interval};
120 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
121 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
122 $opt{'value-format'} = $opt{units} && sub {
124 log(abs $_[0] || 1) / log(10)
125 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
126 + 1e-15 # float imprecision
128 my $decimal = ($unit % 3) == ($unit < 0);
129 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
130 $decimal = ($unit % 3) == ($unit < 0);
131 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
133 3 + ($_[0] < 0), # digits plus optional negative sign
135 $_[0] / 1000 ** int($unit/3), # number
136 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
137 $opt{units}->[$unit/3] # suffix
142 my (@lines, @values, @order);
144 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
147 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
149 $SIG{INT} = \&show_exit;
151 if (defined $opt{interval}) {
152 $opt{interval} ||= 1;
153 alarm $opt{interval} if $opt{interval} > 0;
156 require Tie::Array::Sorted;
157 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
158 } or warn $@, "Expect slowdown with large datasets!\n";
162 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
164 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
166 s/\A\h*// unless $opt{unmodified};
167 my $valnum = s/$valmatch/\n/ && $1;
168 push @values, $valnum;
169 push @order, $valnum if length $valnum;
170 if (defined $opt{trim} and defined $valnum) {
171 my $trimpos = abs $opt{trim};
172 $trimpos -= length $valnum if $opt{unmodified};
174 $_ = substr $_, 0, 2;
176 elsif (length > $trimpos) {
177 # cut and replace (intentional lvalue for speed, contrary to PBP)
178 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
182 show_lines() if defined $opt{interval} and $opt{interval} < 0
183 and $. % $opt{interval} == 0;
186 if ($opt{'zero-missing'}) {
187 push @values, (0) x 10;
190 $SIG{INT} = 'DEFAULT';
193 $opt{color} and defined $_[0] or return '';
194 return "\e[$_[0]m" if defined wantarray;
195 $_ = color(@_) . $_ . color(0) if defined;
201 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
203 @lines > $nr or return;
206 if (defined $opt{hidemax}) {
207 if ($opt{hidemin} and $opt{hidemin} < 0) {
208 $limit -= $opt{hidemax} - 1;
210 elsif ($opt{hidemax} <= $limit) {
211 $limit = $opt{hidemax} - 1;
215 @order = sort { $b <=> $a } @order unless tied @order;
216 my $maxval = $opt{maxval} // (
217 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
220 my $minval = $opt{minval} // min $order[-1] // (), 0;
221 my $range = $maxval - $minval;
222 my $lenval = $opt{'value-length'} // max map { length } @order;
223 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
224 max map { length $values[$_] && length $lines[$_] }
225 0 .. min $#lines, $opt{hidemax} || (); # left padding
226 my $size = defined $opt{width} && $range &&
227 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
230 if ($opt{markers} and $size > 0) {
231 for my $markspec (split /\h/, $opt{markers}) {
232 my ($char, $func) = split //, $markspec, 2;
234 if ($func eq 'avg') {
235 return sum(@order) / @order;
237 elsif ($func =~ /\A([0-9.]+)v\z/) {
238 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
239 my $index = $#order * $1 / 100;
240 return ($order[$index] + $order[$index + .5]) / 2;
242 elsif ($func =~ /\A-?[0-9.]+\z/) {
246 die "Unknown marker $char: $func\n";
255 color(36) for $barmark[$pos * $size] = $char;
258 state $lastmax = $maxval;
259 if ($maxval > $lastmax) {
260 print ' ' x ($lenval + $len);
263 ($lastmax - $minval) * $size + .5,
264 '-' x (($values[$nr - 1] - $minval) * $size);
266 say '+' x (($range - $lastmax) * $size + .5);
273 color(31), sprintf('%*s', $lenval, $minval),
274 color(90), '-', color(36), '+',
275 color(32), sprintf('%*s', $size * $range - 3, $maxval),
276 color(90), '-', color(36), '+',
280 while ($nr <= $limit) {
281 my $val = $values[$nr];
282 my $rel = length $val && $range && ($val - $minval) / $range;
283 my $color = !length $val || !$opt{palette} ? undef :
284 $val == $order[0] ? $opt{palette}->[-1] : # max
285 $val == $order[-1] ? $opt{palette}->[0] : # min
286 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
287 my $indicator = $opt{indicators} && $opt{indicators}->[
288 !$val || !$#{$opt{indicators}} ? 0 : # blank
289 $#{$opt{indicators}} < 2 ? 1 :
290 $val >= $order[0] ? -1 :
291 $rel * ($#{$opt{indicators}} - 1e-14) + 1
295 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
296 print color($color), $_ for $indicator;
299 print $indicator if defined $indicator;
302 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
303 sprintf "%*s", $lenval, $val;
304 color($color) for $val;
306 my $line = $lines[$nr] =~ s/\n/$val/r;
307 if (not length $val) {
311 printf '%-*s', $len + length($val), $line;
312 print $barmark[$_] // $opt{'graph-format'}
313 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
319 say $opt{palette} ? color(0) : '' if $opt{spark};
325 if ($opt{hidemin} or $opt{hidemax}) {
326 my $linemin = $opt{hidemin};
327 my $linemax = ($opt{hidemax} || @lines) - 1;
330 $linemax = @lines - $linemax;
332 printf '%.8g of ', $opt{'sum-format'}->(
333 sum(grep {length} @values[$linemin .. $linemax]) // 0
337 my $total = sum @order;
338 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
339 printf ' in %d values', scalar @order;
340 printf ' over %d lines', scalar @lines if @order != @lines;
341 printf(' (%s min, %s avg, %s max)',
342 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
343 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
344 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
353 show_stat() if $opt{stat};
354 exit 130 if @_; # 0x80+signo
362 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
365 -a, --[no-]ascii Restrict user interface to ASCII characters
366 -c, --[no-]color Force colored output of values and bar markers
367 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
369 --header Prepend a chart axis with minimum and maximum
371 -H, --human-readable Format values using SI unit prefixes
372 -t, --interval[=(N|-LINES)]
373 Output partial progress every given number of
374 seconds or input lines
375 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
376 -L, --limit[=(N|-LAST|START-[END])]
377 Stop output after a number of lines
378 --graph-format=CHAR Glyph to repeat for the graph line
379 -m, --markers=FORMAT Statistical positions to indicate on bars
380 --min=N, --max=N Bars extend from 0 or the minimum value if lower
381 --palette=(PRESET|COLORS)
382 Override colors of parsed numbers
383 -_, --spark Replace lines by sparklines
384 --indicators[=CHARS] Prefix a unicode character corresponding to each
386 -s, --stat Total statistics after all data
387 -u, --unmodified Do not reformat values, keeping leading whitespace
388 --value-length=SIZE Reserved space for numbers
389 -w, --width=COLUMNS Override the maximum number of columns to use
390 -h, --usage Overview of available options
391 --help Full pod documentation
392 -V, --version Version information
398 barcat - concatenate texts with graph to visualize values
402 B<barcat> [<options>] [<file>... | <numbers>]
406 Visualizes relative sizes of values read from input
407 (parameters, file(s) or STDIN).
408 Contents are concatenated similar to I<cat>,
409 but numbers are reformatted and a bar graph is appended to each line.
411 Don't worry, barcat does not drink and divide.
412 It can has various options for input and output (re)formatting,
413 but remains limited to one-dimensional charts.
414 For more complex graphing needs
415 you'll need a larger animal like I<gnuplot>.
421 =item -a, --[no-]ascii
423 Restrict user interface to ASCII characters,
424 replacing default UTF-8 by their closest approximation.
425 Input is always interpreted as UTF-8 and shown as is.
427 =item -c, --[no-]color
429 Force colored output of values and bar markers.
430 Defaults on if output is a tty,
431 disabled otherwise such as when piped or redirected.
433 =item -f, --field=(<number> | <regexp>)
435 Compare values after a given number of whitespace separators,
436 or matching a regular expression.
438 Unspecified or I<-f0> means values are at the start of each line.
439 With I<-f1> the second word is taken instead.
440 A string can indicate the starting position of a value
441 (such as I<-f:> if preceded by colons),
442 or capture the numbers itself,
443 for example I<-f'(\d+)'> for the first digits anywhere.
447 Prepend a chart axis with minimum and maximum values labeled.
449 =item -H, --human-readable
451 Format values using SI unit prefixes,
452 turning long numbers like I<12356789> into I<12.4M>.
453 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
454 Short integers are aligned but kept without decimal point.
456 =item -t, --interval[=(<seconds> | -<lines>)]
458 Output partial progress every given number of seconds or input lines.
459 An update can also be forced by sending a I<SIGALRM> alarm signal.
461 =item -l, --length=[-]<size>[%]
463 Trim line contents (between number and bars)
464 to a maximum number of characters.
465 The exceeding part is replaced by an abbreviation sign,
466 unless C<--length=0>.
468 Prepend a dash (i.e. make negative) to enforce padding
469 regardless of encountered contents.
471 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
473 Stop output after a number of lines.
474 A single value indicates the last line number (like C<head>),
475 or first line counting from the bottom if negative (like C<tail>).
476 A specific range can be given by two values.
478 All input is still counted and analyzed for statistics,
479 but disregarded for padding and bar size.
481 =item --graph-format=<character>
483 Glyph to repeat for the graph line.
484 Defaults to a dash C<->.
486 =item -m, --markers=<format>
488 Statistical positions to indicate on bars.
489 A single indicator glyph precedes each position:
495 Exact value to match on the axis.
496 A vertical bar at the zero crossing is displayed by I<|0>
498 For example I<:3.14> would show a colon at pi.
500 =item <percentage>I<v>
502 Ranked value at the given percentile.
503 The default shows I<+> at I<50v> for the mean or median;
504 the middle value or average between middle values.
505 One standard deviation right of the mean is at about I<68.3v>.
506 The default includes I<< >31.73v <68.27v >>
507 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
512 the sum of all values divided by the number of counted lines.
513 Indicated by default as I<=>.
517 =item --min=<number>, --max=<number>
519 Bars extend from 0 or the minimum value if lower,
520 to the largest value encountered.
521 These options can be set to customize this range.
523 =item --palette=(<preset> | <color>...)
525 Override colors of parsed numbers.
526 Can be any CSI escape, such as I<90> for default dark grey,
527 or alternatively I<1;30> for bright black.
529 In case of additional colors,
530 the last is used for values equal to the maximum, the first for minima.
531 If unspecified, these are green and red respectively (I<31 90 32>).
532 Multiple intermediate colors will be distributed
533 relative to the size of values.
535 Predefined color schemes are named I<whites> and I<fire>,
536 or I<greys> and I<fire256> for 256-color variants.
540 Replace lines by I<sparklines>,
541 single characters (configured by C<--indicators>)
542 corresponding to input values.
544 =item --indicators[=<characters>]
546 Prefix a unicode character corresponding to each value.
547 The first specified character will be used for non-values,
548 the remaining sequence will be distributed over the range of values.
549 Unspecified, block fill glyphs U+2581-2588 will be used.
553 Total statistics after all data.
555 =item -u, --unmodified
557 Do not reformat values, keeping leading whitespace.
558 Keep original value alignment, which may be significant in some programs.
560 =item --value-length=<size>
562 Reserved space for numbers.
564 =item -w, --width=<columns>
566 Override the maximum number of columns to use.
567 Appended graphics will extend to fill up the entire screen.
571 Overview of available options.
575 Full pod documentation
576 as rendered by perldoc.
588 seq 30 | awk '{print sin($1/10)}' | barcat
590 Compare file sizes (with human-readable numbers):
592 du -d0 -b * | barcat -H
594 Memory usage of user processes with long names truncated:
596 ps xo rss,pid,cmd | barcat -l40
598 Monitor network latency from prefixed results:
600 ping google.com | barcat -f'time=\K' -t
602 Commonly used after counting, for example users on the current server:
604 users | tr ' ' '\n' | sort | uniq -c | barcat
606 Letter frequencies in text files:
608 cat /usr/share/games/fortunes/*.u8 |
609 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
610 sort | uniq -c | barcat
612 Number of HTTP requests per day:
614 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
616 Any kind of database query with counts, preserving returned alignment:
618 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
621 In PostgreSQL from within the client:
623 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
625 Earthquakes worldwide magnitude 1+ in the last 24 hours:
627 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
628 column -tns, | barcat -f4 -u -l80%
630 External datasets, like movies per year:
632 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
633 jq .[].year | uniq -c | barcat
635 Pokémon height comparison:
637 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
638 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
640 USD/EUR exchange rate from CSV provided by the ECB:
642 curl https://sdw.ecb.europa.eu/export.do \
643 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
644 barcat -f',\K' --value-length=7
646 Total population history in XML from the World Bank:
648 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
649 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
652 And of course various Git statistics, such commit count by year:
654 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
656 Or the top 3 most frequent authors with statistics over all:
658 git shortlog -sn | barcat -L3 -s
660 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
662 ( git log --pretty=%ci --since=30day | cut -b-10
663 seq 0 30 | xargs -i date +%F -d-{}day ) |
664 sort | uniq -c | awk '$1--' | barcat --spark
666 Sparkline graphics of simple input given as inline parameters:
668 barcat -_ 3 1 4 1 5 0 9 2 4
670 Misusing the spark functionality to draw a lolcat line:
672 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
676 Mischa POSLAWSKY <perl@shiar.org>